Skip to content

Architecture ​

Status: draft v0.1, 2026-11-09. Client evidence in docs/research/client-stack.md. Principle in force: build the simple version of something that can scale, not the scalable version of something simple. We refactor when reality asks, not before.


1. The three ideas everything rests on ​

1.1 The galaxy is a pure function ​

galaxy(seed, coordinates) → star, planets, resources

Deterministic, no I/O, no state. Nothing about an untouched system is stored anywhere. The database only ever holds deltas: what players discovered, claimed, built, mined out or destroyed.

  • Storage for the universe: zero, regardless of size.
  • The client can generate and render any region locally from the seed; the server ships only deltas and the things it is willing to reveal.
  • No world-generation step, no size cap, no migration when the galaxy grows.

The same function must produce bit-identical results on client and server. That constraint alone justifies sharing one language across both.

The one exception, and it is bounded. Rare deposits deplete permanently and re-emerge in unsafe space (ECONOMY.md §2.2), so where the rare things are is not a pure function of the seed — it is seed plus accumulated state. This is fine and must stay fine:

  • The seed still generates the potential: which bodies could ever host a rare deposit. That set never changes and costs nothing.
  • The database stores only which of those have been worked out, and which have surfaced — a delta per touched body, exactly like everything else.
  • Migration is a job (§3), not a simulation: when a deposit depletes, one row is written and one new site is chosen from the seed-defined candidate set, weighted toward unsafe systems.

So the client can still render a region from the seed alone; it simply cannot know where the good rocks are without asking, which is correct — that information is a game mechanic, not geometry.

1.2 Nothing ticks ​

State that changes with time is computed on read from a timestamp, never advanced by a background job.

  • Tax ledger = one row per colony per cycle, written by the daily tax sweep and read as a sum; nothing accrues between cycles.
  • Build queues = started_at + duration; completion is derived, then lazily written when read.
  • Fleet position = interpolated from departed_at, arrives_at, origin and destination.
  • Security and tax = arithmetic over a handful of stored integers (SECURITY-AND-SOVEREIGNTY.md §8).

A million idle players cost nothing. There is no cron that touches every row, ever. This is the single biggest reason this design can reach the scale Jonathan asked for without becoming complicated.

1.3 Geography is the shard key ​

Travel across the galaxy takes days to weeks by design (GAME-DESIGN.md §4.3). Players live in a region; interaction is overwhelmingly local.

So when one database stops being enough, the galaxy partitions by region — and cross-partition traffic is rare, slow, and tolerant of asynchrony, because in the fiction it already is. The sharding strategy is free: it is the map.

We do not build this now. We build so that it stays possible: region id present in every spatial row from day one, no query that assumes a global view, no cross-region transaction in a hot path.

2. Stack ​

LayerChoiceWhy
LanguageTypeScript, client and serverGalaxy generation and combat resolution must be one implementation shared by both. Two implementations of a deterministic function is a bug factory.
RuntimeBunTypeScript with no build step, built-in test runner, bundler, Postgres and SQLite drivers. Fewer moving parts is the brief. [DECISION PENDING] — confirm production maturity vs Node 26 before locking; Node is the fallback and the port is trivial.
HTTPHonoTiny, fast, excellent TypeScript, runtime-agnostic so the Bun bet is reversible.
DatabasePostgreSQL 17One store, boring, already the house pattern on the prod box. JSONB where the shape is genuinely variable; real columns everywhere else.
SchemaSQL migrations, plain files, applied in orderNo ORM. Hand-written SQL with a thin typed query layer.
PushPoll first, SSE when it mattersCheck-in play barely needs push. SSE survives proxies, reconnects by itself, and costs one HTTP connection. WebSockets only if the live-orders layer ships.
AuthPasskeys (WebAuthn), optional recovery emailNo social logins — the no-tracking pillar forbids them. No passwords to leak.
ClientReact 19.3 + Three.js 0.187 (WebGL2) + react-three-fiber 9.8 + Tailwind 4 + Vite 8, installable PWA, no native wrapperOne full-viewport canvas with dense DOM UI over it. Three is 189 KB gzip against Babylon's 694 KB, measured. WebGPU sits behind a createRenderer() seam for later. Evidence and alternatives: docs/research/client-stack.md.
AssetsGLB + meshopt + KTX2, generated by headless Blender 5.2.3 LTSScripted in Python, versioned as code, no human in the loop. See ART.md.

Deliberately absent ​

No Redis, no Kafka, no message broker, no microservices, no GraphQL, no Kubernetes, no ORM, no state container framework. Every one of these is a thing to add the day it solves a problem we actually have, and not before. Postgres is the queue, the cache and the lock manager until it demonstrably isn't.

3. Data model sketch ​

Only the shapes that matter. All spatial rows carry region_id.

TableHoldsNotes
accountscredentials, recoveryminimal PII by policy
charactersidentity, reputation, skillsthe player's face in the world
systemsdelta only: discovery, controlling colony, paying-the-Empire flag, taxed_atrow exists only once touched
bodiesdelta only: depletion, structures presentrow exists only once touched
structuresgates, refineries, fabricators, defencesthe gate rows are also the lane graph
fleetscomposition, location or transit, doctrine
transitsfrom, to, departed_at, arrives_at, modeposition is derived, never stored
queuesproduction jobs: started_at, duration, outputcompletion derived, lazily materialised
stacksinventory at a location
ordersmarket orders, per station
contractshauling, mercenary, collateral
corporations, memberships, standingspolitics
reportsplayer-facing event log, combat logsthe check-in loop reads this first
jobsdue_at scheduled workthe only tick in the system

The only scheduler ​

One table, jobs, with a due_at index. A worker claims rows with SELECT ... FOR UPDATE SKIP LOCKED and runs them. It handles exactly three things:

  1. Arrivals — a transit completes: resolve combat, apply loot, write reports.
  2. Tax sweep — daily, over claimed systems only. That set is bounded by what the playerbase can afford to hold, which is the only set that matters.
  3. Lapse and decay — the lapse ramp, structure ruin, contract expiry.

This is one small worker process, horizontally scalable by adding more of them, with no coordination beyond SKIP LOCKED. It is the entire background infrastructure of the game.

Read a system and the API derives its current state from a timestamp and a handful of integers. Nothing in this picture polls, ticks, or sweeps — the worker only ever wakes for work that is genuinely due.

4. Shared core ​

A package used verbatim by client, server and the balance simulator:

  • galaxy/ — the seeded generator. Deterministic, exhaustively tested for determinism, versioned: changing it changes the universe, so it gets an explicit version number and a migration story.
  • combat/ — (fleets, doctrines, environment, seed) → log. Pure. Tested properly (COMBAT.md §9).
  • economy/ — tax, security, refining, travel time, price band. Pure arithmetic, tested properly.
  • rules/ — shared constants and types.

Everything else — the HTTP layer, the UI, persistence — is plumbing around these four. Plumbing gets smoke-tested, not unit-tested (AGENTS.md §3).

5. Security posture ​

  • Server-authoritative without exception. The client may predict anything (it has the galaxy function, it can render futures) but it never reports an outcome.
  • Every mutation validated against server-held state. The client is assumed hostile.
  • Rate limits on every client-callable endpoint, by account and by IP.
  • Visibility is enforced server-side: the API must never return what a character has not discovered or cannot see. Intel is a game mechanic (SECURITY-AND-SOVEREIGNTY.md §7), so a leak in the API is a leak in the design.
  • No third-party scripts on the client. None. This is a pillar, not a preference.

6. Scaling path ​

Written down so nobody builds step four on day one.

  1. Now — one app process, one Postgres, one worker. Comfortably serves tens of thousands of check-in players on the existing hardware.
  2. Pressure on reads — read replicas; the app is stateless, so add processes.
  3. Pressure on writes — partition by region_id. Cross-region interaction is already rare and slow in the fiction.
  4. Hot spots — the market and chat are the only plausible candidates for extraction into their own service. Not before they hurt.

Observability comes from the Prometheus/Loki/Grafana stack already running on the prod box (INFRA.md). Server-side, aggregate, anonymous — never per-player tracking.

7. Open questions ​

  • [ ] Bun or Node 26 for the server runtime. Decide with evidence before first code.
  • [ ] How is galaxy-function versioning handled if we ever need to change generation? Freeze v1 for existing regions and generate new space with v2? Probably.
  • [ ] Does the client ship the galaxy function (fast, offline-capable, but reveals generation rules) or ask the server (slower, hides nothing worth hiding)? Leaning: ship it — the seed-to-star mapping is not a secret, what is out there and who owns it is.
  • [ ] Postgres as the market matching engine — fine at this scale, but worth a load test early since it is the most write-contended thing in the game.
  • [ ] Replay delivery: seed + order list and let the client re-simulate, or ship the resolved log? Re-simulation is a few hundred bytes and proves determinism in production. Leaning re-simulation.

Changelog ​

  • 2026-11-09 — v0.1. Pure-function galaxy, compute-on-read state, geography as shard key, single-language TypeScript core shared client/server, Postgres-only persistence, one jobs table as the entire scheduler.
  • 2026-11-09 — v0.2. §1.2 no longer describes resource accrual, which GATHERING.md §6 forbids: compute-on-read now covers queue completion, transit position and the tax ledger. Deposit migration targets unsafe systems rather than low-security ones. The structures row loses beacons, and the lapse job no longer claims security decay.

Galaxy design documentation. Nothing here is implemented yet.