Beyond Doubt
Architecture
Six thousand lines of TypeScript, no runtime dependency in the engine, and one rule that decides most of the structure: the thing that says a move is legal must be the same thing on both sides of the wire.
Five layers, and which way they point
Dependencies only ever point inward
The core knows nothing about paintings, guests, Portuguese or HTTP. It is pure functions over integers, which is why the same file decides a move in the browser and decides it again on the server. There is no second implementation of the rules to keep in sync, and no way for the two to drift apart under a deadline.
A board is an integer, a clue is a predicate
core/grid.ts · core/clue.ts · core/solver.ts
Up to twenty squares, two states each, one bit per square: the whole board is a 32-bit integer. A grid precomputes the masks that clues talk about — each row, each column, the corners, the edges, the interior, the neighbours of every square, the squares between any two.
There are nine kinds of clue and eleven ways to select squares, and every clue compiles to a closure with one method: given this world, are you satisfied? Counting clues become two or three bit operations. Each compiled clue also carries a cost, so the solver runs the cheap tests first and the expensive one — is this set of squares connected? — only sees candidates that already survived everything else.
Enumerate every world still consistent with what the player knows, then see which squares agree across all of them. Squares that agree are forced; the player may flip those and nothing else. That one rule is the no-guess guarantee, and it is also what makes the game safe to race — nobody can gamble ahead.
Enumeration walks the submasks of the unknown squares, which is why the board is capped at twenty-one free squares: beyond that the walk stops being instant. The limit is enforced rather than assumed — the solver throws instead of quietly getting slow.
Boards are built backwards
core/generate.ts
The solution is drawn first, and the generator only ever considers clues that are true of it. From that pool it assembles a chain — an opening fact, a clue that forces a square, the clue that square was holding, onward until the board is fully determined. Each clue records its gate: the square whose reveal hands it to the player. An opening clue has no gate. If the chain cannot be completed the board is discarded and another is drawn, so nothing ships that the machine has not solved first.
What crosses the wire
net/protocol.ts · net/client.ts · net/server.ts
The client is sent a grid, the tags on each square, a label seed, and the opening clues. Not the solution, not the forced path, not the board's seed. It deduces locally and answers instantly; the server independently re-derives the same deduction and records it. The client sends a mode, never a date or a seed, so it cannot ask for tomorrow's board or one it has already solved offline.
| Route | What it decides |
|---|---|
| /api/today | which boards exist right now, in the game's timezone |
| /api/play/start | resolves a mode to an edition; returns a view with no answer in it |
| /api/play/move | the only place a square is ever decided; returns unlocked clues |
| /api/play/hint | clue first, square second — the ladder is server-side |
| /api/auth/request · verify | emailed code, single use, ten minutes, rate limited both ways |
| /api/account/export · delete | the copy you are owed, and the exit |
| /api/admin/flags · flags/set | 404 unless an operator token exists |
Eighteen routes in total, all of them POST-with-JSON except the handful that are
genuinely reads. One file answers all of them, and the same file is what
npm run serve runs locally.
Two databases, one interface, one test suite
net/store.ts · net/store-pg.ts
Nine tables: accounts, sign-in codes, sessions, plays, results, rooms, room members,
operator settings, rate-limit hits. The Store interface is roughly thirty
methods; SQLite implements it for local work and Postgres for production, and the
entire server suite runs against both — each Postgres run in its own schema, so the
tests can be run concurrently without colliding.
That is not belt and braces. The two disagree in small, expensive ways: one query that passed happily on SQLite failed on Postgres because a column that is a boolean in one is an integer in the other, and nine tests went red the moment they met the real database.
Time, and who owns it
core/edition.ts
Every board is a pure function of its date and theme, so two people opening the same day's puzzle on opposite sides of the world generate the identical board independently, with nothing passed between them. The archive can therefore serve the board that actually ran on a past day without having stored it.
"Which day is it" belongs to the server, and it is a civil date in the game's own timezone rather than a UTC one — a daily that rolls over in UTC arrives at eight in the evening for anyone in Eastern time. Civil-day arithmetic anchors at noon UTC and steps in UTC, because subtracting twenty-four hours from an instant lands in the same civil day twice on the two days a year the clocks move.
Switches, enforced where they matter
net/flags.ts
Puzzles, the archive, the weekly edition, free play, rooms, new accounts and a notice line can each be switched off without a deploy. Every one of them is checked on the server, in the routes themselves, rather than by hiding a button — a flag that only greys out a control is decoration, because anyone can still call the API. The tests assert the refusal, not the hidden button.
How it is deployed
One function, one bucket, two suppliers
The build produces static pages — the game, this page, the article, the policies —
and a single serverless function that answers every /api/* route by
constructing the same server object the local command does. Postgres is hosted at Neon,
sign-in codes are delivered by Resend. Connection pools are deliberately small, because
a serverless platform runs many instances and a hosted database has a connection
ceiling.
The shape, in numbers
Lines of TypeScript
Themes are 806 lines for all seven together — the point of the skin layer is that the seventh costs about as much as the second. The engine has no runtime dependencies at all; the server has exactly one, a Postgres driver.
Limits worth knowing
The honest list
Twenty-one free squares is a hard ceiling, not a tuning knob — a larger board needs a different solver, not a bigger constant. Rate limiting is deliberately approximate: two requests racing can both squeak through, which for a speed bump on a sign-in form is a fair trade against locking a table on every hit. And the no-guess guarantee is expensive by construction: it is paid for on every single tap, which is exactly why the board is small enough that paying it is free.
Written alongside the code rather than after it. If this page and the source disagree, the source is right and this page is a bug.