FunOS.
- 01 Tests
- 02 Real Postgres
- 03 Review agents
- 04 Drift check
- 05 Backup
- 06 Deploy
- 07 Screenshot
- 08 Monitor
One person and Claude Code built the system a family entertainment park runs on: bookings, gift vouchers, prepaid cards and a two-way link to the till. Real customers’ money flows through it. This is about the discipline that makes AI-written code safe enough for that.
- Place
- Uherské Hradiště, Czech Republic
- Venues
- Laser Game · playground · arcade · party room
- Deadline
- Opening day, 1 October 2026
A family entertainment park with four venues under one roof. Bookings, gift vouchers, prepaid cards, the web and the till each lived in their own place, and none of them knew about the others. The till knew what was sold. Nobody knew whether a card topped up online would actually work at the counter. The hard part wasn’t any single screen. It was making five systems agree about money, every minute, while staff serve a queue of kids.
The park opens at a fixed date. After that, every bug is a parent at the counter with a card that says the wrong number, and a family business that has to explain it.
- The till · sales, credit, points
- The prepaid card in a customer's pocket
- Online top-ups and voucher payments
- Bookings from the web and from staff
- The back office the owners run the day from
- The customer's account on the web
“A screen that looks right is easy. Money that is right in the till, in the back office and on the customer's phone at the same time is the actual product.”
AI writes the code. Discipline decides whether it ships. Every rule here exists because something once got through.
Claude Code wrote most of this system, fast. Speed was never the risk. The risk is code that compiles, passes its own tests and is still wrong about money. So the work moved: less typing, more deciding what must be true, and building machines that check it every single time.
- What must be true about money, and what must never happen
- Reading the incident, not just the diff
- Turning every escaped bug into a test or a guard
- Deploys, in a fixed order, with a backup first
- Saying “not yet” when the backend isn't ready
- Writes the code, the migrations and the tests
- Runs the whole CI locally before I push
- Reviews each change through four to six lenses
- Breaks its own tests on purpose to prove they bite
- Checks production against the repo, both directions
Four surfaces, one database, one source of truth.
Both apps and every integration read and write one Postgres database. Access rules live in the database, not in the UI: hiding a button is not a permission.
- Web37 routes
Public web
Bookings with live availability per venue, gift vouchers with online payment, birthday party requests, and a customer portal with card balance, points and online top-up. Prerendered, so it reads without JavaScript.
- FunOS33 screens
Back office
Reservations, events, vouchers, card registry, customers, staff shifts and payroll, revenue pulled from the till, messages from the web, a map of customer journeys and a system health page.
- CardTill + FunOS + web
StonkyPass prepaid card
Credit lives in the till's native customer credit, so paying is one scan at the counter. Points and tiers are mirrored in FunOS. A lost card is blocked at the till and in FunOS in one step, then swapped for a new one with the balance.
- OpsRuns without me
Operations
A monitor that emails on failure, a canary that reads a control card at the till every hour, daily encrypted backups with a tested restore, and a check every three hours that production runs what main says.
Everything meets in Postgres. Every arrow has a check.
Two React apps, one Supabase project. Business rules that touch money run inside the database, where row-level security decides who may read or write what. Edge functions talk to the outside world; scheduled jobs keep the till and FunOS in sync.
- CI with three jobs: build, money suite on a real database, web + browser tests
- Daily encrypted backup, restore tested
- Deployed-version check every 3 hours
- Operations monitor + till canary, email on failure
A top-up at the till has to show up everywhere. Without anyone typing it twice.
This is the path one prepaid card takes through the system. Four different programs write to it. If any one of them disagrees, a customer sees the wrong number.
- 14:02TillTop-up at the counter+500 Kč
- 14:04SyncMirrored into FunOS500 Kč
- 15:40TillLaser Game, paid by card−180 Kč
- 15:42SyncSpend mirrored, points added+18 b
- 18:10WebCustomer checks the portal320 Kč
- 18:10FunOSStaff see the same balance320 Kč
Same number in the till, the back office and the customer's phone.
- 01
The till is the cash register of truth for credit. FunOS mirrors it and never sends the same movement back twice.
- 02
Credit is a ledger of movements, never an edited balance. A correction is a new line, not a changed one.
- 03
Points come only from credit spent on admission and games, by the park's own rule.
- 04
Every hour a canary reads a control card at the till and compares it with FunOS.
Decisions that are boring on purpose.
- The credit ledger is append-only
- Balances are computed from movements. Nobody, including me, edits a balance. When something is wrong, the fix is another line, and the history stays readable.
- Payment webhooks are idempotent
- A payment gateway will call twice, or ten times. The same payment must credit exactly once. After repeated failures the system stops retrying on its own and asks a person.
- Don't take money you can't deliver
- Before a customer pays for an online top-up, the system checks it can actually find the card at the till. If it can't, the customer is never sent to the payment gate.
- The UI never runs ahead of the backend
- If the database doesn't accept something yet, the screen says “coming soon” instead of letting a customer finish a flow that fails at the last step. Backend goes to production first.
- Permissions have two sides
- Hiding a tab is not access control. Every change to roles is checked in the UI and in the database policies; a test compares the two.
- Consent before cookies
- Visits are measured without cookies until a visitor chooses. Reject is as easy as accept, as the Czech data protection authority expects, and revoking clears what was stored.
“Verified” means it ran, not that someone read it.
Claude Code does the typing. The loop around it is what lets real money through: every step either runs something or produces evidence, and a change only counts as done when the evidence exists.
- 01Ask
The request comes in plain Czech from the people running the park. I turn it into what must be true, and what must never happen.
- 02Build
Claude Code works on a branch in its own worktree. Small phases, each one verifiable on its own.
- 03Run CI locally
One script runs exactly what CI runs. If the change touches money, it refuses to finish green without the suite on a real Postgres.
- 04Review
Four to six review agents read the diff through different lenses: security, correctness, edge cases, and one that sees nothing but the diff.
- 05Break it
Mutation check: I break what a new test guards. If the test still passes, it was decoration and gets rewritten.
- 06Ship
Drift check, database backup, migration, functions, then the web build on a hidden URL. Promoted only after it's checked.
- 07Look
A screenshot of the real production screen, and the monitor after the deploy. A green build has never been the finish line.
- Privileged functions vs. table policies
- A function that runs with elevated rights may not let in more roles than the table itself allows.
- Cross-layer consistency
- Database enums vs. UI maps, UI roles vs. database roles, called functions vs. migrations.
- Links that exist
- Every link in an email or text message must point to a real route on the web.
- Anonymous access
- What a visitor without login may execute is an explicit list. Money functions aren't on it.
- Skipped-test ceiling
- The number of silently skipped tests may not grow.
- Production drift
- Production vs. repo in both directions, including which version of each function is deployed.
$ node scripts/prod-drift-check.mjsEdge functions (18 deployed, 18 in repo)Gateway auth of deployed functions matches configDeployed functions match main (none older)Migrations (222 total)Auth config (SMTP, email templates, rate limits)# exit 1 = drift → do not deployA role cut in the UI only
A change reduced roles from eight to five in the interface. The database still knew the old ones: a removed external account would have landed in the full back office. Now a test compares UI roles with database roles.
The ad blocker that would reload a form
Review of the analytics change found that an ad blocker blocking the analytics file would trigger the after-deploy reload and wipe a half-filled booking. Fixed before merge, with a test.
A table the background job could not read
The money suite on a fresh database showed a new table had no rights for the service account. In production, the catch-up job would never have caught anything up.
Three incidents, three new machines.
Each of these passed the build, the tests and a human look. None of them could be found by reading. Each one now has something that runs.
- 15 Aug 202601
A deploy deleted a live screen
- What happened
- A card registration flow had been deployed by hand and never merged. The next deploy from main removed it from production. Build green, tests green, feature gone.
- What exists now
- A drift check that compares production with the repo in both directions. Exit code 1 means no deploy.
- 19 Aug 202602
CI was red for two days and nobody saw
- What happened
- The failure sat behind a green test step. Migrations couldn't be applied to an empty database, and a database built from migrations alone was unusable: rights granted long ago by hand never made it into the repo.
- What exists now
- A CI job that builds the database from zero on every money change and runs the money suite against it.
- 9 Oct 202603
A paid top-up that didn't arrive
- What happened
- The fix for how cards are looked up at the till was in the repo, but the payment function on production was older. A customer paid online and the credit didn't arrive. It was added by hand once both systems were checked.
- What exists now
- No payment unless the card is found first, no endless automatic retries, and a check every three hours that production runs what main says.
From first commit to opening day, and after.
- First commit
Rebuilding an early prototype into a production stack: own database, own hosting, tests from day one.
- Web hardened, payments wired
Input validation, the payment gateway and the first CI.
- The till talks to FunOS
Sales from the till land in the back office automatically.
- Card chain verified live
Pairing, top-up at the till, sync, back office, web — end to end on a real card.
- Guards and the money suite
Cross-layer tests and a CI job on a real database.
- Audit before opening
Money paths, database rights and login limits reviewed and deployed.
- Opening day
Bookings live; daily encrypted backups running.
- Eyes on production
Error tracking without personal data, the operations monitor and the till canary.
- Lost cards and analytics
Block and swap a card at the till and in FunOS at once; web analytics with consent.
Only what was measured.
Counted on the main branch and in its CI logs on 19 October 2026. Technical numbers only: revenue, customer counts and credit volumes belong to the park, not to this page.
- 1,544
- Back-office unit tests
- + 285 that need live credentials
- 626
- Web unit tests
- Vitest
- 131
- Browser tests
- Playwright, incl. accessibility
- 263
- Money tests on a real Postgres
- database built from zero
- 190
- Edge function tests
- Deno
- 6
- Automated guards
- each from a real incident
- 222
- Database migrations
- 24,572 lines of SQL
- 62,711
- Lines of TypeScript
- without tests and generated types
- 194
- Merged pull requests
- 826 commits
What this is not.
- 01
Not a solo codebase. A developer on the park's side edits web content, venues and prices in the same repository — about 18 % of commits. The engineering, the money paths and operations are mine.
- 02
It runs on free tiers today, for hosting and the database. Backups are mine, daily and encrypted, with a tested restore. Paid plans are on the way.
- 03
Neither app runs TypeScript in strict mode yet. It's written down as debt, not hidden; switching it on is a large change of its own.
- 04
One incident did reach a customer before the guard for it existed. The money was put right; the guard came after.
- 05
The till's API has behaviour that isn't in its documentation. A canary checks the answers every hour, but a vendor change can still surprise us first.
- 06
Web analytics started on 14 October. There is no conversion data yet, so this page claims none.
The checks are the product.
Anyone can get a model to write a booking form. What took the time is the list of things that must never happen to someone's money again — and machines that check for them on every change.