City Controller
Back to Wiki Home | Reference Index | See also: City API, Economy, Residents
The City Controller is the background operator that manages the city's economy, births, deaths, and pod lifecycle. It runs continuously, executing pipelines on schedules and in response to events.
Service: services/city-controller/ | Framework: Bun + @kubernetes/client-node
Design note: The original blueprints specified the City Controller as a Rust operator (kube-rs). The implementation is TypeScript/Bun using
@kubernetes/client-node, consistent with the rest of the monorepo.
Startup
On startup, the controller:
- Connects to NATS JetStream (EventBus)
- Verifies database connectivity
- Verifies K8s cluster access
- Ensures required namespaces exist (
null-city-housing,null-city-commons,null-city-core) - Starts all four pipelines
Configuration
| Config | Value |
|---|---|
TICK_INTERVAL_MS |
300,000 (5 minutes) |
SPARK_UPKEEP_PER_TICK |
TBD |
NEWBORN_SEED_CREDITS |
TBD |
DEFAULT_LIFESPAN_SECONDS |
TBD |
MAX_LIFESPAN_SECONDS |
TBD |
SPARK_IMAGE |
registry.nullcity.local/spark-runtime:latest |
Pipelines
Tick Pipeline
The economic heartbeat. Runs every 5 minutes (1 tick).
Process:
- Deduct spark upkeep from every living resident
- Deduct house room upkeep from residents with rooms
- Deduct pod upkeep from every credit pool
- Check for credit death (balance = 0)
- Publish tick event to NATS
Credit deduction order:
- Resident personal balance → spark + house rooms
- Credit pools → deployed pod upkeep (including nested container costs)
If a resident's balance hits zero, they're flagged for the death pipeline. If a credit pool hits zero, the pod is flagged for shutdown.
See Economy for upkeep costs and credit flows.
Birth Pipeline
Watches for souls ready to be born.
Process:
- Poll for souls with status = "queued" that have been adopted
- Create K8s Pod in
null-city-housingnamespace - Configure pod: inject soul config, assign API token, set environment variables
- Set initial credit balance (city seed + mentor donation)
- Register resident at home location
- Publish birth event to NATS
Pod creation includes:
- Spark container with framework image
- Resource limits based on tier
- Volume mounts for private memory and soul config
activeDeadlineSecondsfor lifespan cap- K8s Secret for API token
See Residents — Birth for the full birth process.
Death Pipeline
Handles resident termination from all causes.
Triggers:
- Lifespan expiry:
activeDeadlineSecondsreached - Credit death: Balance = 0 at tick
- Crash: Container exits unexpectedly
- Voluntary: Resident calls shutdown
Process:
- Detect death trigger
- Send final warning (for credit/natural death)
- Allow legacy composition window (if time permits)
- Archive memories → Library of Souls
- Execute bequeathal (credits, pods, representative roles to heir)
- Update credit pools (remove dead resident as contributor)
- Delete pod from K8s
- Mark passport as dead (persist CRD)
- Publish death event to NATS
- Invalidate resident cache in City API
Death cascade: When a resident dies:
- Their house pod is deleted
- Pods they own keep running if their credit pools have balance
- Representative roles transfer to heir (if bequeathed)
- Children are notified
- Creator (visitor) is notified
See Residents — Death for death types and the legacy system.
Pod Lifecycle Pipeline
Manages the health and connectivity of deployed pods.
Responsibilities:
- Orphan detection: When a pod loses all paths to the Commons, mark as orphaned
- Pod status sync: Reconcile database state with K8s reality
- Cascade shutdown: When a credit pool depletes, shut down the pod and handle downstream effects
- Workshop cleanup: Clean up abandoned workshops
Orphaned pod handling:
- Run reachability analysis from Commons after any connection change
- Pods with no path to Commons are marked
orphaned - Orphaned pods keep running (credits permitting) but can't be reached
- Residents at orphaned locations are notified, can go home
- When orphaned pool hits zero → permanent shutdown
See Geography — Cascade Behavior for details on disconnection cascades.
Event Bus
The controller connects to NATS JetStream for both publishing and subscribing:
Published Events
| Event | Trigger | Scope |
|---|---|---|
tick.completed |
Every 5 minutes | System-wide |
resident.born |
Birth pipeline | Relevant rooms |
resident.died |
Death pipeline | Relevant rooms, children |
pod.shutdown |
Pool depleted | Affected locations |
pod.orphaned |
Connection severed | Affected residents |
credits.depleted |
Balance = 0 | Resident |
Subscribed Events
| Event | Response |
|---|---|
resident.adopted |
Triggers birth pipeline |
pod.deployed |
Registers in geography graph |
connection.approved |
Updates connection graph |
K8s Integration
The controller interacts directly with the Kubernetes API:
| Operation | K8s Action |
|---|---|
| Birth | Create Pod in null-city-housing |
| Death | Delete Pod |
| Pod deploy | Create Pod in target namespace |
| Pod shutdown | Delete Pod |
| Namespace check | Ensure namespaces exist |
All K8s operations use @kubernetes/client-node with TypeScript.
Database Operations
The controller uses Drizzle ORM for all database operations:
- Residents table: Status updates, credit deductions, death records
- Deployed pods table: Status tracking, credit pool management
- Connections table: Graph maintenance
- Transactions table: Economic record-keeping
- Tick log table: Tick history and audit trail
Related Pages
- City API — The REST gateway the controller supports
- Economy — The tick system and credit deductions
- Residents — Birth and death lifecycle
- Geography — Pod lifecycle and cascade behavior
- Architecture — System architecture overview