Paradise · Control Center
June 2026 → July 202617 min read

I started this project after watching Paradise, the Dan Fogelman series: 25,000 people living sealed inside an underground city in Colorado, three years after a catastrophe. The plot is a murder investigation, but what stayed with me after the episode was the housekeeping. How you feed a sealed city. Where its oxygen comes from. What it costs in electricity to light crops that never see the sun. The show does not answer those questions, and it does not have to. I wanted to see whether I could answer them in numbers.
Paradise · Control Center is therefore the command post of that city. The simulation runs continuously server-side, in 1:1 real time, and the browser does nothing but watch it live.
Computed figures, never placed by hand
That was the constraint I set at the start. Almost no figure shown in the interface exists in the code: they are all computed, every simulated hour, from about a dozen physical constants. And those constants are not invented either, they come from real sources, written down in a document in the repository with their formula before being coded.
| Constant | Value | Where it comes from |
|---|---|---|
| Oxygen breathed | 0.80 kg per person per day | 24-hour average, sleepers included, cross-checked between the NASA life-support reference and respiratory physiology |
| Carbon dioxide released | 0.95 kg per person per day | Computed from the oxygen figure through the measured respiratory ratio, in the same sources |
| Water used | 75 L per person per day | Sum of household uses (30 shower, 12 flush, 12 laundry, 8 washbasin, the rest cooking and cleaning), compared against NASA's water-usage tables |
| Water recycling rate | 88 % | What a well-run urban network achieves, far from the 93 to 98 % of the Space Station. That leaves 9 L per person to find every day |
| Food | 2,400 kcal ingested, 2,688 to produce | Human energy requirements, FAO and WHO, averaged by age and sex, plus 12 % processing losses |
| Growing area | 45 m² per person | The area needed to grow those 2,688 kcal every day. Feeding people takes far more plants than recycling their air: 12 to 15 m² would be enough for oxygen alone, as in the BIOS-3 experiment |
| Crop lighting | 90 kWh per 1,000 kcal | Middle of a 78 to 116 range cross-checked over three independent works, including an estimate on vertical-farm wheat. The horticultural LED efficiency behind that figure, 2.7 µmol/J, comes from this review |
| Reactor | 64.7 MWe per module | A small NuScale reactor delivers 73.5 MW to the grid. Since no reactor runs all year (refuelling, maintenance), I only count the 88 % the IAEA observes on average, i.e. 64.7 MW I can rely on |
| Heat released | 120 W per person | 24-hour average metabolic output, from the "heat gain from occupants" chapter of the ASHRAE reference handbook |
| Deep rock | a stable 14 °C | Ground temperature a few tens of metres down, which acts as a passive heat sink |
The rest follows from those values. Between the reserve and the reclamation plant's capacity, the city can sustain about 28,000 inhabitants on water. The Eurostat household-type distribution, applied to the age pyramid, gives 10,901 households averaging 2.29 people. The employment rate applied to the 18 to 64 bracket gives about 10,500 working people. And because the greenhouse area is dictated by calories, it far exceeds what air alone would need: the crops release 2.8 times more oxygen than the inhabitants breathe.
Here is how each circuit works, in the order in which they influence one another.
Air
Every inhabitant consumes 0.80 kg of oxygen a day and releases carbon dioxide. Not at a steady rate: consumption follows a 24-hour activity profile, so people breathe less while asleep. The same profile digs the night troughs and lifts the curves in the morning.
The greenhouses take that carbon dioxide to build plants, releasing oxygen in the process. Nothing enters or leaves the city: carbon cycles between the air, the food and the plant waste that gets composted, and the total never moves. It sits at 135 tonnes, and it does not drift by a gram over thousands of simulated hours, because stocks are counted as integers rather than decimal numbers.
The real problem with a sealed atmosphere is not running out of oxygen, it is that plants and humans do not exchange their gases in the same proportions: oxygen accumulates and carbon dioxide is depleted. Composting the plant waste is what catches that mismatch up, by returning its carbon to the air.

Food
The greenhouses produce on demand: what the city has just eaten, plus enough to top up a 10-day buffer. They therefore do not run flat out all the time, and that is what makes any supply problem visible straight away.
Three things can cap the harvest: the growing area, which is fixed, the electricity available for lighting, and the carbon dioxide present in the air. That last ceiling matters: without composting, the plants would eventually run out of raw material.
The operator has only one food lever, a ration setpoint. It reduces what everyone eats, so it stretches the reserves when production drops. Its floor is set at 70 %, because the minimum dietary energy requirement used by the FAO is 1,680 kcal per adult per day, i.e. 70 % of the nominal 2,400. Below that, the setpoint is refused.

Electricity
The city's demand comes in three parts: the greenhouses at 252 MW continuous, the inhabitants at roughly 1.2 kW each (housing, lifts, ventilation, water treatment), and the cooling groups. The greenhouses alone are therefore 83 % of consumption, which settles the size of the plant: seven small reactors, six under load and one to cover refuelling.
When capacity drops, the order of service matters more than the total. Life support is powered first, and the greenhouses take what is left. A reactor failure therefore does not cut the air, it reduces the harvest, and famine arrives weeks later. It would take a near-total collapse, with capacity falling below vital needs, for the city to run short of air.
Cooling is ranked alongside the greenhouses and never among the vital needs. In practice, stopping or starting cooling groups changes how much food gets produced, but it can never cut the ventilation.
Water
Every inhabitant uses 75 litres a day, but water runs in a closed circuit and 88 % of it is recovered. That leaves 9 litres per person to replace every day, 225 m³ for the city, and it is the only genuinely consumable resource.
The first version made it a countdown: a 180-day reserve draining slowly, and the city dying on schedule. I did not want that, but I did not want to remove the constraint either. So I added equipment, sized to demand like everything else: an on-site reclamation plant producing up to 252 m³ a day, enough to cover the make-up of 28,000 inhabitants. Below that threshold the reserve stays stable and the 180 days become a safety buffer again; above it, the reserve drains and scarcity appears on its own.
What leaves the reserve is not lost to the calculation, it goes into a second reservoir of waste water, and reclamation makes the return trip. The sum of the two never changes.
Heat
Heat comes from four places: the inhabitants themselves at 120 W each, the greenhouse lighting which ends up almost entirely as heat, the reactors and their distribution losses, and the life-support pumps. It is removed by the cooling groups and by the rock, which absorbs the more the zone sits above its 14 °C.
Temperature is not recomputed for display, it is a value that advances hour by hour and keeps its past. It takes about 8 hours to react, matching the inertia measured for a large underground volume against rock. As a result, stopping a cooling group does not make a counter jump: temperature climbs gradually, and it eventually settles on a plateau, because the hotter the zone gets the more the rock absorbs. Restart the group and it comes back down at the same pace.
The circuit closes back onto electricity: removing heat consumes power, and that power is then missing from the greenhouses. A hotter city therefore produces slightly less food.
Births and deaths
Every simulated hour, each inhabitant has a chance of dying that depends on their age: a constant part, for accidents, and a part that grows with age and doubles roughly every eight years. The age pyramid at founding is not drawn by hand, it is deduced from that same rule.
Births depend on the age structure. A woman can have a child between 15 and 49, with the highest chance between 25 and 29, and the total is set against an index of 2.1 children per woman, the replacement level. The population curve is therefore written nowhere: it comes out of the calculation, age band by age band, set against mortality.
Scarcity acts on both sides. The city continuously checks which of its four vital resources is tightest (food, oxygen, water, life support) and derives a sufficiency level from it. When that level drops, there are first fewer births, then additional deaths, in numbers proportional to the share of the population the resources can no longer feed, spread over 60 days, a duration drawn from the physiology of fasting. The mechanism switches itself off: as the population falls, resources become sufficient again.

That is the result I wanted to see. Cutting 55 % of greenhouse capacity takes the population from 25,000 to about 12,600, where it settles at the level production can feed without ever going extinct. Stopping all 12 greenhouses puts the food break 9.5 days out; dropping the ration to 70 % pushes it to 13 days and 4 hours, the ratio of a division by 0.7, which is written nowhere.
Everyone's day
Every inhabitant has an occupation, a household and a routine. The 25 occupations are derived from the real needs of the infrastructure: so many people are needed to run the greenhouses, the reactors, the water plants, the schools, healthcare, security, maintenance. Services that run around the clock are covered by three crews rotating weekly. Meals and bedtimes are correlated within a household, so flatmates eat together.

None of this is stored per inhabitant. Position on the map, current activity and life history are recomputed on demand from the world seed and the hour, anchored on a real building and interpolated during commutes. The map never ships 25,000 positions at once, only a sample of the visible frame.

This level of detail sets traps. At noon a reactor's shift crew switched to the "meal" state, and the control room was counted as having zero occupants. Staff of critical facilities are now counted on duty across their whole shift window, on-site breaks included.
Replaying the same history
The state of the city is a function of its past: the world seed, the elapsed time, and the list of operator interventions. Nothing else. Saves on disk are therefore only a cache: replaying ten years from founding takes 216 ms, so you can always do without them. The prototype that validated this architecture, before a single line of the server, generates the 25,000 inhabitants in 11.7 ms and holds the population registry in 17 bytes per person, against 128 for a full object.
The front end computes nothing
The repository is a Bun monorepo in three pieces.
| Package | Role |
|---|---|
packages/protocol |
The shared contract: exchanged types and derivation of the city clock |
apps/server |
Elysia on Bun: hosts the engine, exposes REST and SSE, persists in bun:sqlite |
apps/web |
Vite, React 19 and Tailwind v4: presentation only |
The browser holds no business rule and above all no clock advancing on its own: the clock is derived in the shared package, imported by both the engine and the interface, so only one implementation exists. When the link degrades, the front end freezes the time on the last known truth and says so, instead of ticking out seconds nobody computed. Tables run on TanStack Query, Table and Virtual, with filtering and pagination done by the engine, and navigation state lives in the URL through nuqs, so every view is shareable as is.
My first project written entirely by an AI
This is the first project I carried end to end without writing the code myself. All of Paradise was produced by Claude Code from my prompts. What I did: decide what gets built and in what order (the specification runs to 138 KB and 16 modules, cut into deliverable batches), pick the technologies and settle the evaluations, arbitrate the design before the first line of code, and set quality criteria I did not bend on.
| Criterion | What it requires |
|---|---|
| Green gate | Tests, type-check, lint and front-end build. Nothing is declared delivered otherwise |
| No invented values | An unsourced figure postpones the feature |
| Browser verification | Every module actually exercised, no console error, other views checked for regressions |
| Accessibility | An alert always comes with text or a symbol, so it reads without colour and without sound |
| Adversarial review | After each module, agents hunt for defects while others try to refute them. I keep only what survives |
What I thought of it
Early on, the exercise is very good for clarifying ideas. Stating an intention and getting it back as closed questions forced me to settle things I would have left vague. Research is the other obvious gain: the resource model described above would not exist without it, because finding, cross-checking and documenting figures from physiology, agronomy and nuclear engineering would have been a project of its own. Then there is production speed, and a very short feedback loop: I said a stream of readings was drowning the real events in the journal, and it was gone within the half hour.
After about twenty iterations I grew tired of it, and the reason is simple: I no longer had a grip on what was happening. By the end I could not have told you how a good part of the code worked. If I picked Paradise back up today I would struggle, and I would need serious reading time before touching anything. That is a real cost, and it settles in gradually, without warning. The green gate and the adversarial reviews compensate in part, because they check things I do not re-read, but they are no substitute for knowing your own code.
For this particular project the trade is still well worth it. The goal was personal: to watch a city live. I got the pleasure of seeing it evolve week after week, at a level of detail I would never have reached otherwise, and it was genuinely entertaining. On a project I have to maintain over time or share with others, I would go about it differently.
Where it stops
| Version | v0.28, paused since 26 July 2026 |
| Tests | 230 of 230 |
| Code | about 25,600 lines of TypeScript |
| Modules delivered | Command deck, population, bracelets, surveillance, journal and audit, sound design, life support, stocks, plus the tactical map, buildings, temperature and life history |
| Modules never started | Dome and lighting, health, governance, emergency protocols, security, surface, assistant |
| Toolchain | TypeScript 7.0.2, Biome 2.5.5, React 19.2, Tailwind v4, Elysia, bun:sqlite |
The state is green and reproducible: tests, type-check, lint and build all pass, the server boots on a genesis of 25,001 inhabitants, and on restart the city clock resumes exactly where it left off.
Why I stop here
The project is not abandoned, it is dormant, and for a reason I had not foreseen when I started: by dint of building levers, I realised that what interested me was watching, not steering. Cutting a bracelet, freezing a camera, rationing a city, that was the original idea, the one that came from the show. What keeps me in front of the screen is the age pyramid drifting on its own and the stock break announced in nine and a half days without my touching anything.
The project stays as it is in its frozen state, tests green and base reproducible. If I ever pick it up again, I would start a good part of the code from scratch, and I would change two things in the way I work. First, cut the logic into much smaller, isolated pieces, each testable on its own: that is what keeps you in control of a project this size. Second, guide and review every technical choice one by one, and above all look closely at how each figure is produced and computed, because that is where real understanding of the model sits, and it is what I was missing by the end.
The subject itself still interests me a great deal. This engine could serve as the base for other life-simulation projects, in one form or another.
What I take away
The "no hard-coded values" rule takes more work than it sounds. It forbids nudging a model towards a pleasant result, so it forces you to find the sources, write them down, and sometimes postpone an entire feature for lack of data. In exchange, every displayed figure traces back to a cause, and the behaviours I find interesting (a population stabilising instead of dying out, the oxygen surplus, the stock break receding when you ration) showed up without my programming them.