DOCS · docs/architecture.md

Architecture

What runs where, what depends on what, and the one rule that keeps the site honest: the engine is the product, and everything else is a consumer of it. If a number on a page disagrees with sim/engine.ts, that is a bug — not a preference, and not a tuning exercise.

The pieces

PieceWhereRuns inReaches outside?
The enginesim/engine.ts, sim/hash.tsa browser, Node, CIno
The gamessim/war.ts, sim/warbots.ts, sim/outbreak.tsa browser, Node, CIno
The siteapp/one Node process, plus the visitor's browseronly the three live-season routes below
The live seasonapp/lib/live/, app/api/live/the serverRobinhood Chain RPC, the drand beacon
The daily boardapp/lib/board/, app/api/runs/the serverthe data directory only
The daily anchorsscripts/anchor.ts, scripts/verify-anchor.ts, app/lib/anchors.tscronRobinhood Chain RPC
The daily postscripts/post-daily.tscronthe X API
The proofscripts/replay.ts, tests/any checkout, CIno

The dependency rule

Dependencies point one way: sim/ → app/ → scripts/. Nothing in sim/ imports anything but its own hash module, and nothing in sim/ can reach a network, a subprocess or the environment.

That is not a convention, it is a test. `tests/boundary.test.ts` walks every tracked source file and fails the build if a capability that could break the boundary appears outside a twelve-entry allowlist — the anchor writers and their memo helper, the daily poster, the live season's chain, store and drand client, the board's files, and the three browser clients that call this site's own API. Each entry carries its reason beside it, so adding one is visible in the diff instead of silent.

One engine, three runtimes

The same module is loaded three times, and the point of the design is that all three agree:

RuntimeWho runs the engineWhy it matters
The visitor's browserthe homepage dish, /play, /worm/[id], /player/[address]the page can be recomputed by the reader instead of believed
The server/api/run, app/lib/daily.ts, the live season's resolverthe commitment the server publishes is the one the browser checks
CInpm test, npm run replay -- --checka drift fails the build, not the site

There is no second implementation to keep in step. A page that needs a number imports the engine; a page that quotes a fact reads it from a file (`app/page.tsx` reads docs/STATUS.md, and counts the tests in tests/ rather than printing a number someone has to remember to update).

The process

One Node process, no cluster, no worker pool: next start -H 127.0.0.1 -p 3230 under a systemd unit with ProtectSystem=strict and ReadWritePaths limited to the data directory. Nginx terminates TLS and forwards X-Real-IP. There is no queue, no cron inside the app and no second service to keep in sync — the cron jobs are three shell entries, and they are listed in the README.

What is on disk

Everything the server remembers lives under WORM_DATA_DIR, outside the release tree so a deploy cannot take it with it:

season-<n>.json the live ledger: sides, commits, reveals, the day's drawn block board/<YYYY-MM-DD>.json one day's leaderboard entries runs/<id>.json one submitted run's input record, so anyone can replay it

Writes are serialised in process and land with a rename, so a crash cannot leave half a file. One process is assumed; a second one writing the same directory is unsupported, and says so in app/lib/live/store.ts.

Where the chain enters

Exactly two things touch a chain, and neither can move money:

1. The draw. At 18:00 UTC the server reads Robinhood Chain (chain ID 4663) for the first block mined after the cutoff, and that block's hash becomes part of the day's seed. It is a read. 2. The anchors. scripts/anchor.ts writes one zero-value memo per closed day — worm:1|day|date|seed|ticks|chainHash — signed by a dedicated wallet that only pays its own gas. The verifier recomputes the day locally and compares.

Everything else about the chain is a reader: /anchors lists the manifest, /api/live reports the draw, and scripts/verify-anchor.ts checks a memo back.

Why the numbers on the site cannot drift silently

Claim on a pageWhere it comes from
The dish, the chain head, the event logcomputed from the engine at build time
The test countcounted from tests/*.test.ts at build time
"What does not exist"quoted from docs/STATUS.md at build time
Launch facts (token, venue, contract)read from config/launch.json; undecided values render as "to be announced"

The pattern is the same in each case: one source, read rather than restated.


Other docs — Status · Determinism · The live season · Design · Outbreak · For modifiers · Glossary · Deploy and ops · The source reading · Audit log

All docs · github.com/wormsim/worm

Rendered directly from docs/architecture.md in the repository at build time. This page is a view of that file, not a copy of it. All docs.