08 — Services & Platform
This document specifies the Every Last City platform layer: the account and profile model, cross-device cloud save and settings sync, the online game service (correspondence and cloud async play under the Orders/Cascade turn structure, the turn-bundle drop-box, workshop hosting, sharing), the notification fan-out, statistics and leaderboards built on the Victory system's Standing, Tenure and Track records, the simulation-farm and AI-pipeline infrastructure, telemetry and privacy, the self-hosting story, and operational requirements for the official hosted service. Protocol-level multiplayer behaviour lives in 05-multiplayer.md; content and package formats live in 07-modding-content.md; the save/replay container this platform stores and syncs is defined by 03-architecture.md; the turn structure the async service exists to carry is defined by 10-turn-model.md; the records the statistics system reports on are defined by 14-victory.md.
Status: Draft v0.2 · Owner: unassigned · Depends on: docs/design/00-direction.md, docs/design/01-decision-turn-model.md, 03-architecture.md (the save container, determinism, the CLI and its farm verb, plugin runtime config), 05-multiplayer.md (wire protocol, authority modes, fog enforcement, invites, stall policy), 06-ai.md (the AI seat contract and the self-play tuning pipeline), 07-modding-content.md (package formats, the WASM sandbox, upload validation), 10-turn-model.md (Orders, the Cascade, the Reckoning, deadlines, reserve banks, absence and substitution), 13-command.md (Attention Events and the Dispatch), 14-victory.md (Tracks, Standing, Tenure, the Seal record, the Chronicle, placings), 04-ui-ux.md (the screens this document supplies content for)
1. Platform principles
SVC-010 Guest-first: the game MUST be fully playable without any account or network connection. Solo play, hotseat, LAN/direct-connect play, local saves, local mods, the map/scenario editor, and the unit-set editor MUST all work for an offline guest profile. No feature may nag a guest to sign up more than once per install.
Rationale: an account is a network dependency, and a patient single-player strategy game is exactly the thing people play on a laptop with no connection. Requiring identity to start a solo game buys the platform nothing and costs the player the whole product when the service is unreachable. Guest-first is also the honest position given SVC-970: if nothing may ever require an account, the offline path has to be the primary one rather than a degraded fallback.
no-test-needed: there is no account or sign-in surface anywhere in this tree — nothing under any package implements identity, login, or an upgrade prompt — so guest-first holds by absence: every game path in the suites runs offline, local play constructs with no transport at all and every server-backed game goes through the lobby explicitly. It becomes testable when §2's account machinery lands: a solo game must then start and complete on a fresh install with no account object present.
SVC-020 A cloud account MUST be free, and MUST be required only for the four capabilities that
inherently need durable server-side identity: (a) cross-device sync of saves/settings/history,
(b) participating in server-hosted games — cloud async and correspondence play, the turn-bundle
drop-box (SVC-448), and Internet real-time relayed or authoritative through an @everylastcity/server
instance (MP-050), (c) publishing to the workshop, and (d) appearing on leaderboards. Browsing and
downloading public workshop content MUST NOT require an account (subject to rate limits, SVC-465). An
instance-scoped guest identity (SVC-167) satisfies (b) only where the instance's account policy permits
it; it never satisfies (a), (c), or (d).
no-test-needed: none of its four gated capabilities exists yet — no cross-device sync (§4), no server-hosted game service beyond the pre-launch lobby, no workshop publishing, no leaderboards (§7) — and there is no identity anywhere in the tree to gate with, so the partition binds nothing. The clause that does bind code today is its own exclusion, which holds by SVC-010's note: nothing requires a network or an identity to play. It becomes testable as each capability lands, each then owing its anonymous-twin check (browsing and downloading public workshop content staying account-free per SVC-465).
SVC-030 Local-first: every piece of account-scoped data (profiles, settings, saves, history) MUST have its authoritative working copy on the player's device and remain usable with the service unreachable. The cloud is a sync/replication target, never a gate. Loss of connectivity MUST NOT lose player data or block offline features.
no-test-needed: there is no cloud half anywhere in this tree for local-first to constrain between — nothing under packages/server persists anything account-scoped — its one store is MP-160's game log, a local file container keyed by operator configuration; accounts and sync do not exist — so every piece of account-scoped data this build holds already lives on the device with no replication target. Until §4's sync lands there is no gate for a test to hold; when it does, the device copy must remain authoritative through an outage the service cannot reach.
SVC-040 Self-host parity: everything the official service does MUST be achievable by self-hosting
the published @everylastcity/server Docker image (§10) — one image, containing both network surfaces
(SVC-401). The official hosted instance MUST run the same code and image that is published for
self-hosting. Features MUST NOT be gated to the official instance except where §10 explicitly notes an
operational exception.
no-test-needed: there is no published Docker image in this tree — none exists anywhere under any package — and the official service runs from its git checkout directly rather than from a self-hostable artifact, so parity has nothing to compare yet. It becomes testable when SVC-800 lands: the same image running the pre-launch lobby's committed configuration must then answer identically to what this tree deploys by hand.
SVC-050 Boring infrastructure: @everylastcity/server MUST be a single Node 22 LTS application
written in TypeScript (strict), whose only required external dependencies are PostgreSQL 16+ and a
blob store (local filesystem by default, any S3-compatible store for scale). PostgreSQL MUST be the only
supported relational database: a second SQL dialect would mean two schemas, two migration paths, and two
sets of query semantics to verify, and mechanical verification is the safety net this project depends
on. Redis MAY be used for room presence and horizontal scale (SVC-407) but MUST remain optional — a
single-process instance MUST work without it. No other infrastructure component (queues, search engines)
may be introduced into the base spec without a revision here. Zero-configuration self-hosting survives by
shipping PostgreSQL inside the published image (SVC-800), not by supporting a second engine.
Rationale: small-studio scope; one process + one DB + one bucket is operable by one person. Losing an
embedded-database default is a real cost, but it is paid once in image size and first-run time (SVC-800)
rather than continuously in dialect drift across every query the service issues.
no-test-needed: the durable service it sizes is unbuilt — packages/server today has no database, no blob store and no queue; its manifest declares exactly one external runtime dependency (ws) beside its workspace siblings — so every clause binds nothing except by absence. It becomes testable when §5's game service lands: then that service's dependency set, and the image's contents under SVC-800, are what this row checks against.
2. Identity and accounts
2.1 Local profiles
SVC-100 The client MUST support multiple local profiles per install. A profile MUST exist before play begins; the client MUST auto-create a default guest profile on first run so play is zero-friction.
Rationale: three separate needs converge on the same object. Machines are shared. Hotseat play (10-turn-model.md TM-460) puts several people at one keyboard and each of them wants their own keybindings and their own record. And a player who wants to try a strategy without it landing in the record they care about needs somewhere to put it (SVC-630).
no-test-needed: no profile machinery exists in the client yet — there is no per-profile store anywhere under packages/client/src for multiple profiles to enumerate, and no auto-created default guest profile either — so neither clause stands up for a test to hold. It becomes testable with the profile object this section defines: two profiles playing alternately on one install keeping their own settings, presets and record.
SVC-110 Each profile MUST carry a permanent unique identifier (UUIDv7) generated at creation. Display names MUST NOT be required to be unique.
Rationale: uniqueness on a human-chosen name is a tax with no benefit at profile level — identity is the UUID, the public handle of SVC-155 is the thing that must be unique, and two people called "Ironside" on one machine is not a problem worth solving.
no-test-needed: it binds the profile object of SVC-100, which does not exist in this tree yet — no per-profile store under packages/client/src carries an identifier to check. It becomes testable with that object: two profiles created on one install must carry distinct generated UUIDs while sharing a display name.
SVC-120 A profile MUST store, at minimum: display (commander) name; accent/theme colour (§3); options/settings document; keybinding profiles; per-device-class input layouts (desktop/touch/gamepad); saved setup presets, map-generator option presets, AI option presets, and saved victory Terms configurations (14-victory.md §12.5); notification preferences; and game history/statistics records (§7).
no-test-needed: it is the field catalogue for the profile object that SVC-100 does not yet have in this tree, and most of its cells are surfaces unbuilt here as well — notification preferences (§6), game-history and statistics records (§7) — so nothing in the list stands up on its own to be tested. It becomes testable when the profile lands: an export round-trip carrying every named field intact.
SVC-130 The in-game commander name MUST be changeable at any time, independent of the profile's identity, and MAY differ per game.
Rationale: the name on the profile is administrative and the name on the empire is part of playing. Binding them would mean a player who wants to be someone else this game has to create a second profile and lose their record to do it.
no-test-needed: the name this row governs has no store in this tree — profiles are unbuilt (SVC-100) and no client surface edits or persists a commander name — so the independence clause binds nothing yet. It becomes testable with the profile object: renaming mid-install must leave existing records keyed by identity rather than by name.
SVC-135 Profiles MUST be exportable to and importable from a single portable file (see SVC-190). Import into an existing profile MUST merge rather than replace (SVC-660).
Rationale: an export that cannot be merged back is a snapshot, not portability — it forces a player moving between two machines to nominate one of them as the real one forever. Merging is possible here by construction because history records are keyed by game UUID (SVC-660).
no-test-needed: it exports the profile object that SVC-100 does not yet have in this tree, so there is no archive to produce or merge. The container half its saves would ride on stands today — 03-architecture.md's save representation pinned at packages/client/test/save-container.test.ts under AR-070 — and the import-must-merge half waits with SVC-660.
2.2 Cloud accounts and linking
SVC-140 Any local profile MUST be promotable in place to a linked cloud account without data loss: on linking, the profile's UUID becomes (or is aliased to) the account identity and all local data eligible for sync is uploaded. Unlinking MUST leave a fully functional local profile with all data intact.
no-test-needed: it joins a local profile to a cloud account, and neither end exists yet — no profiles (SVC-100) and no accounts anywhere in this tree — so promotion and unlinking have no subject. It becomes testable when §2.2 lands: linking must then leave every byte of local data intact, and unlinking must leave a fully playable offline profile behind.
SVC-150 One cloud account MUST be linkable on any number of devices concurrently. Each linked device appears in a device list with name, platform, and last-seen time; the player MUST be able to sign out any device remotely.
no-test-needed: the device list it governs does not exist — there is no account to link devices to anywhere in this tree, and nothing under packages/server persists a session or a device record (its only state today lives in the process). It becomes testable with sessions per SVC-170: remote sign-out must then revoke that device's tokens.
SVC-155 Cloud accounts MUST have: an immutable account UUID; a unique, changeable public handle (rename cooldown SHOULD be 30 days); a non-unique display name; an optional verified email address; and zero or more linked OAuth identities. Handles are the invite/leaderboard identifier; display names are cosmetic.
no-test-needed: no account exists anywhere in this tree to carry a UUID, handle, display name or OAuth identity — nothing under any package implements identity — so the field catalogue binds nothing. Its forward consequence already shapes what stands: with no public handle to name anyone, invitations travel as auto-generated join codes instead, which MP-470's pins title in packages/server/test/lobby-transport.test.ts.
SVC-160 Accounts MUST require a stated age of 13+ (16+ where local law requires); guest profiles carry no age gate. The service MUST NOT knowingly retain personal data of children below the threshold.
no-test-needed: no account exists anywhere in this tree and guest profiles (SVC-100) are unbuilt too, so the age gate has nothing to bind — there is no sign-in form to state an age on. It becomes testable with SVC-165's email sign-in: below-threshold ages must then be refused at account creation and no personal data retained.
2.3 Authentication and sessions
SVC-165 The official service MUST support passwordless email sign-in (one-time code or magic link) as the baseline method. OAuth providers (Steam, Apple, Google) MAY be added; if any third-party OAuth login is offered in the iOS client, Sign in with Apple MUST be offered there (App Store requirement). The service MUST NOT store user-chosen passwords for the official instance. Self-hosted instances MAY enable a local username+password mode for closed groups (Argon2id-hashed) since SMTP may be unavailable (§10).
no-test-needed: the official service has no sign-in of any kind yet — nothing under packages/server implements authentication (its lobby admits by join code, MP-470), so neither the passwordless baseline nor its no-stored-passwords half binds anything today. It becomes testable when identity lands per §2.3: a one-time-code sign-in must work while no user-chosen password is stored anywhere on disk.
SVC-167 Instance-scoped guest access: an instance's account policy (open / invite / guest — MP-1200)
MAY offer guest identities so a player can be invited into a game on that instance without creating a
durable account. (A guest identity is a server-side thing, distinct from the purely local guest
profile of SVC-010/SVC-100, which touches no server at all.) A guest identity MUST be: minted only
from a valid invite capability token or join code (SVC-420, MP-470); scoped to the single server
instance that issued it and to the games it was invited to; authenticated by a guest token presented
in Auth (MP-270) with the same session rules as SVC-170 (short-lived access token, rotating revocable
refresh token bound to one device record). A guest identity MUST NOT require an email address, MUST NOT
be usable on another instance or device, MUST NOT be a workshop publisher, MUST NOT appear on
leaderboards or be rated (SVC-640), and MUST NOT receive cross-device sync (SVC-320) — i.e., only
SVC-020(b) is unlocked. Lifetime: a guest identity MUST survive as long as it holds a seat in a live
game on that instance, plus an instance-configurable idle expiry (default 90 days) after its last game
ends; on expiry the seat history remains in the game record under the guest's display name. A guest MUST
be able to upgrade in place to a full cloud account on that instance, keeping its seats, history, and
notification bindings (the server-side analogue of SVC-140). Guests are subject to the same age
statement as accounts (SVC-160) and to instance moderation (SVC-480, SVC-645). The official instance
MUST default guest access to off, so SVC-020's account gates apply there unless the operator
deliberately enables it; self-hosted instances choose their own policy. Rationale: 05-multiplayer.md
builds on instance-scoped guests (MP-050, MP-270, MP-1200); this is the identity model those
requirements assume, kept deliberately thin so guests never become a second, unmanaged account tier.
no-test-needed: it defines a guest identity no code in this tree mints — nothing under packages/server creates a guest token, device record or expiry timer; the lobby admits by join code, but that code authorises presence rather than an identity (MP-470). Every clause therefore waits on §2.2–2.3's machinery: instance scoping, session rules beside SVC-170, the workshop and leaderboard prohibitions over surfaces this document owns elsewhere, idle expiry, and in-place upgrade to an account as the server-side analogue of SVC-140.
SVC-170 Sessions MUST use short-lived access tokens (≤ 1 hour) plus rotating, revocable refresh tokens bound to a device record. All tokens MUST be revoked on remote sign-out or account deletion. All API traffic MUST use TLS 1.2+.
no-test-needed: there is no token machinery anywhere in this tree — nothing under any package issues, presents or revokes an access or refresh token (the lobby's only authentication is presenting the current join code, MP-470) — so session lifetimes and the TLS floor have no subject. It becomes testable with SVC-165's sign-in: tokens must then be short-lived, rotating, and revoked on remote sign-out.
SVC-175 Per-game passwords, password checks on load, and passwords embedded inside transferable game files MUST NOT exist anywhere in this design. Access to a server-hosted game is governed by account identity plus per-game membership and invite capability tokens (05-multiplayer.md).
Rationale: a secret that travels inside the file it protects authenticates the file, not the person holding it, and it is disclosed to everyone the file is ever forwarded to. It also cannot be revoked, rotated, or audited, and it produces the worst possible failure — a player permanently locked out of a months-long correspondence game by a forgotten string. Real membership does all of the work and none of the harm.
no-test-needed: an absence claim over the whole set that this tree already honours — no session or game password exists under any package's source; the only masked input in the client is hot-seat's device-local hand-off PIN (rendered type=password on the occlusion-handoff screen pinned at packages/client/test/handoff.test.ts), which protects nothing about access to a game. What governs instead does not exist yet: identity plus membership tokens per §2.3 and 05-multiplayer.md's invite model. It becomes checkable the moment any surface would store or ask for a game password — this row is what fails.
SVC-180 New-device sign-in MUST trigger a security notification (§6) to the account's verified email.
no-test-needed: it needs two machines that do not exist in this tree — device records with sign-in events (SVC-150/SVC-170) and the notification fan-out (§6) — so there is no event to raise or deliver. It becomes testable when both land: a fresh-device sign-in must then produce exactly one security notification on the verified address.
2.4 Data portability
SVC-190 "Export everything": the client (and, for linked accounts, a service endpoint) MUST produce a
single archive containing, in documented open formats: profile metadata, all settings and keybinding
documents, all saved setup/generator/AI/Terms presets, every save (as 03-architecture.md containers —
initial state plus order log), all bookmarks/annotations, the full game-history record set including the
per-turn series of SVC-605, each finished game's stored Terms paragraph (VC-2000) and complete Chronicle
(VC-2450), workshop publication metadata, and the telemetry consent record. The archive MUST be
importable by any Every Last City client and by any @everylastcity/server instance. What "every save"
resolves to for a server-hosted game is bounded by MP-730 and not widened by this endpoint: for a live
authoritative fog-enforced game the export carries that seat's own fog-filtered view cache (AR-590),
never the container or the log (MP-250); the full container becomes exportable only where MP-730 says it
does. Solo, hotseat, relay-only, and bundle/drop-box games export in full.
Partially held 2026-09-28: the save-format half of the archive stands pinned; most of the catalogue names surfaces this tree does not have yet. Every save is 03-architecture.md's container — immutable initial state plus append-only order log — and that representation stands pinned at packages/client/test/save-container.test.ts under AR-070, so the archive's core payload has its witness today. What stands nowhere yet is everything else in the list: profiles (SVC-100), game-history records with per-turn series (§7), workshop publication metadata, a telemetry consent record — and no service endpoint to export from exists anywhere under packages/server either. It becomes testable as each cell lands: the archive must then carry it in documented open form, bounded for live games exactly where MP-730 says — that seat's fog-filtered view cache, never the full container.
SVC-195 Nothing in the export may be proprietary-encrypted: a player with only the published format documentation (03, 07) and no Every Last City software MUST be able to parse it.
no-test-needed: it constrains an export that does not exist — SVC-190's archive has no producer in this tree, client-side or service-side — so there is nothing to check for proprietary encryption. The formats its parseability would ride on are already open and pinned (the save container under AR-070 at packages/client/test/save-container.test.ts; package formats owned by 07-modding-content.md). It becomes testable with SVC-190: a player holding only the published format documentation must then be able to read every byte of the archive.
3. Settings and administration
04-ui-ux.md owns the screens. This section owns scope, storage, and sync semantics — which setting lives where, and what may never be stored at all.
SVC-200 Settings MUST be organized into exactly three scopes, each a versioned document: device (video/resolution, audio device, performance, and the data location where the platform has one — SVC-250), profile (gameplay options, interface mode, keybinds, accent colour, notification prefs — sync-eligible), and per-game (view preferences such as per-position colour overrides). A setting MUST live in exactly one scope, and SVC-260 is the normative assignment for every setting that plausibly belongs to two.
no-test-needed: it organizes settings into scopes, but no scope documents exist in this tree — the client's settings are one local model (packages/client/src/settings/model.ts) with no device/profile/per-game split and no versioning envelope — so exactly-three-scopes has nothing to partition yet. It becomes testable when §3's storage lands: every setting must then resolve to exactly one scope per SVC-260's table.
SVC-210 All audio controls, including title/intro music volume, MUST live in the single audio settings panel.
Rationale: a volume control reachable only from an administration screen is a defect, not a feature. One panel per concern is the rule, and audio is the case where the temptation to make an exception is strongest because the title music plays before a profile is loaded — which is a loading-order problem to solve, not a reason to scatter the control.
no-test-needed: there are no audio controls in this tree at all — zero matches for audio machinery under any package's source, so no volume dial exists to consolidate and the single panel would have nothing to hold. It becomes testable when sound lands: every audio control, title music included, must then appear in one panel only.
SVC-220 New-profile defaults: the device MUST hold an editable "defaults for new profiles" template (options + keybinds), applied at profile creation only and never as a live override of an existing profile.
Rationale: on a shared machine the person who sets up the game wants the next person's profile to start sensibly, but a template that kept applying itself would silently undo deliberate per-profile choices. Creation-time-only makes the template predictable and makes "reset to defaults" a distinct, explicit act.
no-test-needed: it is a template for profile creation (SVC-100), which does not exist yet — there is no new-profile event anywhere under packages/client/src to apply defaults at. It becomes testable with profiles: the device-scope template must then shape only new creations, never live-editing an existing profile's settings.
SVC-230 Profile accent colour: each profile MUST have an accent colour/theme applied to out-of-game client chrome (menus, dialogs). In-game colouring is determined by the player position, not by the profile, so that every participant sees the same board in the same colours. The player MUST be able to override the accent independent of any preset palette.
Partially held 2026-09-28: the out-of-game accent-colouring stands at its own pin; per-profile separation waits on a profile object this tree does not have yet. The colour mechanism is built and pinned — packages/client/test/chrome-colour.test.ts titles UX-100 (which colour governs, on the live shell), which tints the out-of-game chrome from the client's single local settings document, overridable in place of any preset palette. What this row adds beyond it stands nowhere yet: profiles (SVC-100) do not exist in this tree, so there is no per-profile separation to check — until they land, the one standing mechanism serves the one implicit local player only.
SVC-240 Hosting defaults survive only as self-hosted server configuration (MP-470, MP-1200;
SVC-810) — not as a profile setting. No profile, device, or per-game setting may store a session/game
password: lobbies are joined by auto-generated invite link or join code (MP-470) and per-game passwords
are prohibited outright (SVC-175). The client MAY keep one device-scope (SVC-200) preferred listen port
used to prefill direct/LAN hosting (MP-440(b)) and manual host:port connection (MP-450, MP-500); it is
a convenience default with no security role. Decision: 05-multiplayer.md owns the direct-hosting flow
and it has no password concept, so 08 stores nothing that flow would consume.
no-test-needed: it is mostly prohibitions over settings that do not exist in this tree — no scope stores any session or game password (there are none anywhere; SVC-175's absence claim), and hosting defaults survive nowhere as profile state because profiles themselves are unbuilt (SVC-100). The forward consequence already holds by construction: lobbies join by auto-generated code, which MP-470 titles in packages/server/test/lobby.test.ts, precisely because nothing is stored for any such preference to read.
SVC-250 Storage/administration page: the client MUST surface, per platform, where account-scoped data and installed content are held, how much space they occupy, and every action available on them — and MUST NOT present a storage model the running platform does not have. 07-modding-content.md MOD-110 states that obligation for content and MOD-115 states which store each platform uses; this requirement extends the same treatment to account-scoped data (profiles, settings, saves, history) and MUST agree with them rather than restate them:
| Platform | What the page shows |
|---|---|
| Tauri desktop and mobile, self-hosted server, CLI (MOD-115) | The resolved paths, "open in file manager" (MOD-110), and an in-client "move data directory" action |
| Browser client | The origin-private store of MOD-115 as MOD-110 requires it to be shown: usage against the browser's quota, whether persistent storage was granted, export back out as a .elcmod, and a control to clear the store — and no path, because there is none |
Whichever model applies, relocating, exporting, or clearing MUST be an action in the client, never a text file the player is expected to author; forced-resolution and display-mode overrides MUST be normal video settings in the device scope (SVC-260), never an override file.
Rationale: every configuration mechanism that lives outside the settings UI is a support burden and an undocumented API. If a setting is worth having it is worth a control; if it is not worth a control it is not worth supporting. Note precisely what that argues for — the control, not the folder. A resolved path with an "open folder" button is one platform's answer to it, and it is not the primary platform's: 03-architecture.md AR-870 makes the browser client the one every other build wraps, and there both mandated actions are unimplementable, since MOD-115 gives a browser an evictable origin-private store with no user-visible directory and no user-editable file. Mandating the filesystem answer everywhere would therefore have forced either a dead control or a fabricated path onto that client, which is exactly the dishonesty MOD-110 exists to forbid. The outcome the player needs — see where my data lives, see what it costs, act on it without hand-authoring a config file — is platform-independent; only the vocabulary is not.
no-test-needed: it is a storage page for locations and actions that do not exist in this tree — no open-in-file-manager button, move-directory action or quota display anywhere under packages/client/src, whose persistence today is a silent local store (packages/client/src/sw) with no user-facing location at all — and the account-scoped data it would surface are themselves unbuilt (SVC-100). Its content half belongs to 07-modding-content.md's MOD-110/MOD-115 rows. It becomes testable when profiles land: per platform, show what is true and nothing fabricated.
SVC-260 Scope assignment (normative; this table discharges SVC-200's "exactly one scope" claim for every setting that could plausibly be argued into two):
| Setting | Scope | Specified by |
|---|---|---|
| Resolution, renderer, display mode | device | SVC-200, SVC-250 |
| Audio output device | device | SVC-200 |
| Every volume level, title music included | profile | SVC-210 |
| Data location, where the platform has a settable one | device | SVC-250 |
| Preferred listen port for direct/LAN hosting | device | SVC-240 |
| Keybindings, per device class | profile | SVC-270 |
| Accent colour / out-of-game theme | profile | SVC-230 |
| Notification channels, quiet hours, per-game mute | profile (per-game override) | SVC-530 |
| Telemetry and crash consent | device and profile jointly | SVC-700 |
| Saved setup / generator / AI / Terms presets | profile | SVC-120, SVC-320 |
| Defaults applied to newly created profiles | device | SVC-220 |
| Per-position colour override within one game | per-game | SVC-200 |
| Rewind discipline | per-game | SVC-315 |
| Orders deadline and reserve bank | per-game, in the game record | 10-turn-model.md TM-480, TM-490 |
| Stall policy and vacation pool | per-game, in the game record | 05-multiplayer.md MP-990; SVC-560 |
| Instance account policy, listen port/TLS, quotas, retention | server configuration | SVC-810 |
| Any session or game password | nowhere — prohibited | SVC-175, SVC-240 |
no-test-needed: its table assigns scopes to settings, and the scope mechanism itself does not exist in this tree (SVC-200) — nothing under packages/client/src partitions settings into versioned documents, so no assignment can be checked. The one cell with standing content today is the prohibition row: session and game passwords live nowhere, which holds by absence (SVC-175). It becomes testable when the three scopes land: every setting in the table must then resolve to exactly its named column.
SVC-270 Profile-scope settings of a linked account MUST sync across devices (§4). Keybinding profiles MUST be cloud-synced per device class so a desktop layout never overwrites a touch layout.
no-test-needed: it syncs profile-scope settings across devices (§4), and no sync half exists anywhere in this tree — nothing under packages/server stores or carries a settings document between devices. Keybindings are stored locally today (packages/client/src/settings) but move nowhere: until §4 lands there is no replication to test, and per-device-class separation must then hold so a desktop layout never overwrites a touch one.
4. Cloud saves and sync
SVC-300 The sync payload for a game MUST be exactly the 03-architecture.md save representation: an immutable initial state, an append-only order/event log, and optional snapshot caches. Sync MUST be delta-based — the client uploads only records after the server's last acknowledged index — and resumable after interruption. Snapshots are never authoritative and MAY be regenerated rather than transferred.
no-test-needed: it defines a sync payload, and no sync service exists in this tree — nothing under packages/server accepts or acknowledges uploaded records (its persistence, MP-160's own game log, is a local file container that takes no uploads). The representation itself stands pinned: 03-architecture.md's immutable-initial-state-plus-append-only-log container at packages/client/test/save-container.test.ts under AR-070. It becomes testable when §4 lands: uploads must then be delta-based against an acknowledged index and resumable after interruption.
SVC-310 Every synced game MUST retain its full order history for its lifetime. The player-facing timeline supports named bookmarks on any index; restoring a bookmark MUST create a non-destructive branch. Nothing in Every Last City may destructively overwrite prior game history.
Rationale: the whole architecture is event-sourced, so a branch is cheap and an overwrite is a deliberate destruction of information the engine was going to keep anyway. Named archives as the only rollback mechanism — the usual arrangement — makes players hoard save slots to work around a limitation that no longer exists.
no-test-needed: it governs retention for synced games, which do not exist in this tree — the server's persistence is MP-160's own local game log, with no synced history to retain, and no rewind or bookmark surface exists in the client either. It becomes testable when §4 lands: a named bookmark must then restore as a branch, and nothing may destructively overwrite prior history.
SVC-315 Commitment discipline: game setup MUST offer a per-game option "no rewind — bookmarks only", which restricts the rollback UI without altering anything the rules compute. Its default MUST be off in solo and hotseat games and on in every multiplayer game. In server-hosted multiplayer games, rollback MUST additionally be an audited admin action requiring the consent of all active human participants by default, or admin-only when that policy was declared at game creation (MP-740).
Rationale: the turn model asks a player to commit orders before seeing the Cascade (docs/design/01-decision-turn-model.md), and that commitment is where the tension lives. A solo player who wants to rewind and re-issue is spending their own game and the design does not moralise about it — so the default is off. A multiplayer game is not one player's to rewind, which is why the default flips and why consent is required rather than assumed. Restricting the UI rather than the rules keeps the option outside the simulation entirely: no hash, no replay divergence, no rules variant.
no-test-needed: both halves of this row wait on machinery that does not exist — no game-setup surface in packages/client/src offers any rewind policy option, and the audited consent-gated rollback itself is an admin action on a server whose absence 05-multiplayer.md's MP-740 records on its own row. The engine fact it rests on stands pinned: accepted orders resolve nothing until the phase closes, which TM-360 holds — so when both halves land, the option can sit entirely outside the simulation as its rationale requires.
SVC-320 Sync set: for linked accounts the following MUST sync — in-progress and finished solo game saves (opt-out per game), profile-scope settings (SVC-270), saved setup presets, map-generator option presets, AI option presets, saved victory Terms configurations, the last-used setup, bookmarks, game-history records, and workshop subscriptions. Server-hosted multiplayer games live canonically on their server and are cached locally, not synced as personal saves.
no-test-needed: it is a sync set for linked accounts (SVC-140), which do not exist anywhere in this tree — nothing under packages/server stores or replicates any of the named items, so there is no set to hold. Until accounts and §4 land, every item that exists lives locally only (setups, settings, saves) and its standing belongs to whatever document owns it; what stands nowhere yet is the replication itself.
SVC-330 Conflict handling MUST never silently discard data. Settings documents merge per-key with last-writer-wins using hybrid logical clocks; a lost write MUST be recoverable from the document's version history (≥ 10 versions retained). Divergent extensions of the same solo game's order log MUST be kept as two branches and surfaced to the player to choose or keep both.
no-test-needed: it handles conflicts between replicas, and no replica exists anywhere in this tree — nothing under packages/server stores a settings version or an order-log branch for anyone. It becomes testable when §4 lands: a lost settings write must then be recoverable from the document's version history, and divergent extensions of one game's log must surface as two branches rather than one overwriting the other.
SVC-340 Offline queue: all sync operations MUST queue locally and drain automatically when connectivity returns. A device that stays offline indefinitely loses no functionality (SVC-030).
no-test-needed: it queues sync operations, and there is no sync operation in this tree to queue — nothing under packages/client/src enqueues anything for a service (the client's persistence is local-only), so drain-on-connectivity has no subject. It becomes testable with §4: an indefinitely offline device must then lose no functionality per SVC-030.
SVC-350 Encryption: sync traffic MUST use TLS; save blobs and order logs MUST be encrypted at rest server-side (AES-256, keys held by the service operator). Client-side end-to-end encryption of solo saves (operator-blind, passphrase-derived key) MAY be offered; E2E saves are excluded from server-side features that must read state.
no-test-needed: it encrypts sync traffic and at-rest blobs, and there is no sync traffic or server-side blob store in this tree — nothing under packages/server persists bytes to disk (src/floor-seat.ts records the decision), so TLS-for-sync and AES-at-rest bind nothing. The MAY clause's operator-blind E2E half likewise has no producer: crypto in this tree digests and randomizes (the container's SHA-256, the seed lanes) but nothing encrypts with a player-held key anywhere. It becomes testable when §4 lands: blobs at rest must then be unreadable without the service operator's keys.
SVC-360 Quotas: the free tier MUST include at least 1 GB of sync storage per account (configurable per instance; self-hosts choose their own). Order logs are small (a typical game SHOULD be under 5 MB per 03-architecture.md targets), so the quota is expected to be effectively unlimited in practice. At quota, sync pauses with a clear client warning; the service MUST NOT delete player data to reclaim space without explicit consent. Snapshot caches MAY be evicted server-side at any time. Server-hosted game storage — async order logs and observation streams (SVC-410, SVC-413) and drop-box bundles (SVC-448) — is instance-side storage charged to the game, and MUST NOT count against any participant's personal sync quota; running out of personal quota MUST never block submitting orders in a server-hosted game.
no-test-needed: it sets quotas for sync storage, and no sync store exists anywhere in this tree — packages/server holds no per-account bytes (nothing persists past the process), so neither the 1 GB floor nor at-quota pausing has a subject. Its forward clause that does bind code today is exclusion-shaped and stands by absence: server-hosted game storage counting against personal quotas cannot happen, because there is no personal quota to count against yet.
SVC-370 Deleting a game from one device MUST be a sync-visible tombstone with a 30-day undo window before server copies are purged.
no-test-needed: it tombstones deletions across devices, and the devices' copies do not exist — nothing under packages/server holds a game long enough to be deleted anywhere but its process (src/floor-seat.ts). It becomes testable with §4's sync: deleting on one device must then raise a sync-visible tombstone whose 30-day window outlives any server purge.
5. The game service (@everylastcity/server)
5.1 API fundamentals
SVC-400 @everylastcity/server MUST expose a versioned HTTPS JSON API (/api/v1/...) served by
Fastify, described by a published OpenAPI document generated from the same schemas the handlers
validate against — one definition, never a hand-maintained second copy. Alongside it runs the real-time
channel defined in 05-multiplayer.md (SVC-401). All object identifiers are UUIDv7. Breaking changes
require a new major API version; each server MUST advertise at /api/meta its supported versions and
which optional surfaces it actually runs — the real-time room server (SVC-404) and the turn-bundle
drop-box (SVC-448) — and clients MUST interoperate across at least one major version of skew.
no-test-needed: there is no HTTP API anywhere in this tree — packages/server exposes two WebSockets and a plain-HTTP /healthz listener, with no Fastify, no /api/meta and no OpenAPI document (its manifest declares exactly one external runtime dependency, ws) — so versioning, surface advertisement and skew interop have nothing to bind. It becomes testable when §5.1 lands: an instance must then advertise its supported versions and the optional surfaces it actually runs.
SVC-401 Runtime composition: one image, one source tree, two network surfaces. The published artifact
MUST contain both the Fastify HTTP API (§§4–8) and the Colyseus room server that carries the
05-multiplayer.md wire protocol (MP-260), built from the same TypeScript sources and running on Node 22
LTS. The server MUST import the same @everylastcity/core package the client imports; a separate
server-side implementation of any rule is prohibited. One consequence is normative here: because that
one core computes client-side preview, authoritative Cascade resolution (MP-340), and the headless
re-simulation that gates rated results (MP-1180), rated-play verification is only as sound as
cross-runtime determinism. The re-simulation verifier MUST therefore be covered by the cross-platform
determinism CI of 03-architecture.md AR-960, and a Node-versus-browser divergence MUST be surfaced as an
infrastructure fault, never as a player-conduct signal (SVC-645).
no-test-needed: there is no published artifact in this tree for the image clause to hold (none exists anywhere under any package), and neither surface it demands is built — no Fastify API, no Colyseus room server; packages/server's transport is hand-rolled WebSockets whose closed typed vocabulary MP-260/MP-265 pin at packages/server/test/wire.test.ts. Its shared-core clause already holds by construction: both sides import @everylastcity/core as a workspace sibling (packages/server's manifest declares it), and server-side execution of that same core stands pinned under AI-480 in packages/server/test/ai-seat.test.ts — so when the artifact lands, this row's new obligations are the two surfaces themselves.
SVC-402 Colyseus scope (binding): the room server MUST be used for rooms, matchmaking, lobby presence, Orders-phase presence, connection lifecycle, and reconnection — and for nothing else. Its schema state-synchronization feature MUST NOT be used as the transport for game state. Orders travel up and validated events travel down exactly as 05-multiplayer.md specifies (MP-250, MP-260, MP-270); what crosses the room's socket is that protocol's message envelope, not a framework-computed state diff.
| Colyseus MAY carry | Colyseus MUST NOT carry |
|---|---|
| Room membership, lobby occupancy and seat-claim status, per-seat presence including Orders-phase commit markers and remaining time (TM-440), pre-launch lobby chat, matchmaking queries, connection/reconnection lifecycle | Game state in any form: the order log or any segment of it, snapshots, ViewCheckpoint/ViewDelta data, fog-filtered view state, Cascade observation streams (TM-2150), order results, turn outcomes — and in-game chat, which is a protocol message (MP-185, MP-270) |
Where 05-multiplayer.md defines a protocol message for something (MP-270), that message is the contract and schema sync MUST NOT become a second, undocumented channel for it. Presence data carried by schema sync is non-authoritative, MUST carry no fact about the game world (TM-440, TM-2070), and MUST be reconstructible from PostgreSQL and the log; losing all of it at once MUST cost nothing but a reconnect. Rationale: this project is event-sourced — the append-only order log is the truth, and replays, spectating, desync detection (MP-1150), fog enforcement, and mode switching all derive from it. Adopting the framework's headline feature as the state transport would destroy every one of those properties, so it is deliberately declined.
no-test-needed: there is no room server in this tree to scope — nothing under packages/server runs Colyseus or any framework (its only transport is hand-rolled WebSockets), so neither its permission table nor its prohibition binds anything yet. The direction is already honoured in what exists: the wire vocabulary is closed and typed, whole-refusal answers standing at MP-260/MP-265 in packages/server/test/wire.test.ts — no state surface crosses that socket today. When a room server lands, this row's table is what constrains it.
SVC-403 Room lifecycle: a room is an ephemeral, in-memory view onto a durable game, never the
game itself. The service MUST be able to materialize a room for any game on demand from that game's
persisted log (SVC-410, MP-090), and MUST dispose it when the last client leaves. Room creation,
disposal, process restart, deploy, and crash MUST NOT end a game, alter its log, forfeit a seat, or lose
a submitted order: an order MUST be durable before it is acknowledged (MP-160, and the durable-ack
objective of SVC-900), and a re-materialized room rebuilds from the stored log. The framework's own
reconnection window is a transport convenience only — the authoritative resume contract is MP-290, which
MUST behave identically for a client returning after its room was disposed and rebuilt as for one
returning to a room still in memory. Room identifiers MUST NOT be exposed as durable game identifiers;
GameId (MP-100) remains the only identity appearing in links, invites, saves, notifications, and the
API.
no-test-needed: there are no rooms to have a lifecycle — nothing under packages/server materializes or disposes any, so this row's room-shaped clauses have no subject. The machinery underneath now exists: MP-160's pins (packages/server/test/durable.test.ts) rebuild the game from its stored log and resume on the same GameId; and since 2026-09-29 that same store holds this row's strict ordering too — durable before acknowledged: a server wire carries no OrderResults for recorded records before their rewrite has landed (packages/server/test/durable.test.ts), the host-mode half of SVC-900's durable-ack objective standing early. It becomes testable with §5's store: a room rebuilt after a crash must then answer identically to MP-290's resume contract.
SVC-404 Async play MUST NOT depend on a live room. Cloud async and correspondence play — the primary
multiplayer mode (§5.2) — MUST be complete over the HTTP API alone: listing games, fetching a game and
the content that seat is entitled to receive, issuing and revising orders, committing, and receiving
notifications MUST all succeed with no room running, no other participant connected, and the real-time
room server stopped, crashed, absent, or disabled by configuration (SVC-810). The turn-bundle drop-box
(SVC-448) MUST likewise be HTTP-only. An instance MAY be operated with rooms disabled and MUST then
still serve async games, cloud sync, the workshop, notifications, and statistics in full, advertising
the absence at /api/meta (SVC-400); real-time play is the only capability it forgoes. Conversely, a
real-time game's log lives in the same store as an async game's, which is what keeps mode switching a
scheduling change rather than a data migration.
Entitlement is a property of the seat, never of the transport. MP-250 governs what a seat may be served over this HTTP API exactly as it governs the room socket, and this requirement states no exception to it:
- A seat already entitled to the whole log — relay-only, direct-LAN, and bundle/drop-box participants (MP-250, MP-340, MP-730) — MAY fetch the canonical order log and a full-state snapshot over HTTP.
- A seat in an authoritative fog-enforced game — which cloud async games default to (MP-380), so
this is the ordinary case, not an edge case — MUST instead be served only its own compacted
observation stream (TM-2150) plus an AR-500
ViewCheckpointof that seat's view, and MUST NOT be served the canonical order log or any segment of it,RefereeViewdata, or a full-state snapshot, under any circumstance including an explicit request for one (MP-250, MP-290, MP-1150, AR-590). An authoritative-mode client that asks for the log MUST be refused, not accommodated.
The fog-enforcement rules themselves remain owned by 05-multiplayer.md and 10-turn-model.md §14; this clause exists only so the HTTP surface cannot be read as a route around them.
Rationale: correspondence games are designed to run for months to years (MP-630) across restarts, deploys, and outages. Binding a turn submission to a live in-memory room would make the most durable mode in the product depend on the least durable component in the stack. Room independence is a durability property, though — not an entitlement exemption. The SVC-402 table already forbids the log on the room socket; a leak over REST would be the same leak, and the primary async mode is precisely the one that runs fog-enforced.
no-test-needed: it governs an HTTP surface that does not exist — packages/server has no Fastify, no /api/meta and no async game service (its surfaces are two WebSockets plus /healthz) — so room-independence for correspondence play has nothing to hold. The direction is already honoured in what exists: the pre-launch lobby's wire carries only typed protocol vocabulary (MP-260/MP-265, packages/server/test/wire.test.ts). It becomes testable when §5 lands: a full turn must then complete over HTTP with the room server disabled.
SVC-405 Mutation endpoints MUST be idempotent via client-supplied idempotency keys, so order submissions, commits, and uploads can be retried safely over bad links.
no-test-needed: there are no mutation endpoints to make idempotent — packages/server exposes no HTTP API (only WebSockets plus /healthz), so client-supplied idempotency keys have nothing to ride on. It becomes testable with §5.1's API: a retried order submission must then land exactly once against durable storage.
SVC-407 Scale and CPU isolation: Node runs one event loop per process, so the platform MUST NOT resolve a Cascade on the loop that serves requests. Cascade resolution (SVC-413), headless re-simulation for rated verification (MP-1180), map generation, uploaded-package validation (SVC-485), and farm batches (§8) MUST run off the request/room loop — worker threads or separate processes — behind a bounded queue, with saturation surfaced as an explicit 429/503 rather than as unbounded latency (SVC-490, SVC-900). A single-process instance MUST work with no Redis; multi-process or multi-node deployments MUST use Redis for room presence and matchmaking and MUST route a client to the node holding its room. The room is the sharding unit and a single game MUST NOT span nodes.
Rationale: the Cascade is the one genuinely CPU-shaped thing the service does, and it arrives in a burst the instant the last player commits. Every seat is waiting on it at once, which is exactly when the request loop must not be blocked.
no-test-needed: the isolation it mandates is between components that do not exist — there are no worker threads, job queues or Redis integration anywhere under packages/server (the deployed instance is one process resolving on its own event loop) — so off-loop Cascade resolution and saturation-as-explicit-refusal have no subject. It becomes testable when §5's durable service lands: a committed turn must then resolve behind a bounded queue rather than on the request loop.
5.2 Async games, the Orders phase, and the turn-bundle drop-box
The turn protocol, ordering, fog enforcement, and anti-cheat are specified in 05-multiplayer.md; the turn structure itself is 10-turn-model.md's. This section specifies the platform obligations that carry them.
SVC-410 The service MUST host async games server-side: the authoritative record is the game's order log plus membership. There are no player-visible files, inbound/outbound directories, SMTP configuration, or hand-typed position numbers. The service MUST NOT transmit turn data by email: service-originated mail carries links only (SVC-540). This bans service-side SMTP turn delivery, not the player's own hand-off — exporting a turn bundle to the OS share sheet or the player's mail client with the file attached (MP-840) is a permanent supported capability (MP-080) and is unaffected by this requirement. Submitting orders in an async game is an HTTP operation against durable storage (PostgreSQL), never a live-room operation: SVC-404 governs, and a room is materialized only while someone is actually connected to watch (SVC-403).
no-test-needed: the durable service it shapes does not exist yet — packages/server now persists a game, log and seat roster included, in store.ts at MP-160's pins; what this row still waits on is its own surface, and there is no HTTP submission path for an order to ride. Its prohibition half holds by absence today: SMTP appears nowhere in this tree, so no turn data travels by email. It becomes testable when §5 lands: an async game's whole turn must then run against durable storage with no room involved.
SVC-411 The unit of async work MUST be one Orders submission per player per turn, not a baton
passed between seats. The platform MUST expose a turn to a seat as: fetch that seat's entitled view of a
board that will not change for the phase (TM-2050); issue, revise and withdraw order records freely
(TM-330, TM-360); CommitOrders (TM-390); and afterwards fetch that seat's observation stream for the
resolved Cascade (SVC-413). A seat MUST be able to complete all of that in one session, at any hour, with
no other participant present, no live room, and no dependency on any other seat having acted (SVC-404,
TM-560).
Rationale: this is why the turn model was chosen and it is the platform's single most important property. A sequential model costs one network round-trip per unit activation; six players with thirty units each is 180 sequential hand-offs, potentially hours apart, which is not a slow correspondence game but a broken one (docs/design/01-decision-turn-model.md). One submission per player per turn is the shape the service has to be built for, and every other requirement in this section follows from it.
no-test-needed: it shapes async work around a service that does not exist — there is no HTTP endpoint for fetching an entitled view, revising orders or committing (packages/server exposes only WebSockets plus /healthz), so the one-submission-per-seat property has no surface to hold. The turn model beneath it stands pinned in core: free revision until close under TM-360's pin, and deadline behaviour under the TM rows at packages/server/test/deadline.test.ts. It becomes testable when §5 lands: a seat must then complete fetch–submit–commit in one session with no other participant present.
SVC-412 The Orders deadline and the reserve bank MUST be durable service state. 10-turn-model.md puts these clocks outside the core and reduces them to two logged facts (TM-480, TM-490, TM-510, TM-550); this document places the clocks in the service and constrains them:
- Deadline and reserve state MUST survive process restart, deploy, and crash, and MUST be recomputed from durable storage rather than held in process memory.
- The service MUST emit at most one
TurnTimeoutper seat per turn (TM-520) and exactly oneCloseOrdersper turn (TM-430), and both MUST be durable before the Cascade begins (SVC-413). - The service MUST close the phase immediately when every expected seat has committed, without waiting for the deadline (TM-400).
- Where the deadline is
None— the default for solo, two-human and hotseat games (TM-490, TM-500) — the service MUST schedule no timer at all and MUST NOT surface a countdown. - A
TurnTimeoutMUST NOT be presented anywhere in the platform as a forfeit, a penalty, or a conduct signal (TM-540, SVC-645).
Rationale: the clock is the one piece of wall-clock time in the whole system, it is deliberately kept
out of the simulation, and it therefore has no home but here. Making it durable is not a nicety: a
correspondence game whose 48-hour deadline resets on every deploy has no deadline, and one that fires
twice because two nodes both held a timer produces two TurnTimeout records for a seat that missed one
turn.
Partially held 2026-09-28: the live-seam clock mechanics stand pinned under their own rows; what waits is the clock's own durability (the records it issues already persist, at MP-160). The mechanism half stands today: packages/server/test/deadline.test.ts titles TM-520 beside MP-1030 (an absent seat timed out at expiry, once), and packages/host-shared/test/deadline-driver.test.ts pins the spent-reserve timeout exactly once with the close following; closing the moment every expected seat has committed stands at TM-400's pin in packages/core/test/turn/orders-phase.test.ts. What waits is this row's headline clause — durable service state: the clocks still live in process memory alone. What changed is what they issue: a TurnTimeout or CloseOrders once accepted rides the persisted log (MP-160's pins), so at-most-one-per-seat now survives a restart by replay; §5's store still owes the clock itself — remaining time and reserve state recomputed from storage, not re-armed from the operator configuration as a mid-phase resume does today.
SVC-413 Cascade execution and delivery:
- The service MUST NOT begin a Cascade before its turn's
CloseOrdersrecord is durable, and MUST resolve each game's Cascade exactly once per turn. - Cascade resolution MUST run off the request/room loop as a bounded queued job (SVC-407).
- Because the Cascade is a pure function of committed state and the committed order set, a crash mid-resolution MUST be recoverable by re-running it from the durable log to an identical result. The service MUST NOT persist a partially applied Cascade.
- Each seat MUST be served its own compacted observation stream (TM-2150, TM-2160) as a single payload after the Cascade has fully completed (TM-2170), and nothing else. A fog-enforced seat MUST NOT be served the canonical log (SVC-404).
- Observation streams MUST be retained for the life of the game, so that a correspondence player arriving days later can review the previous Cascade during the current Orders phase (TM-2230) without the service recomputing it.
- Playback presentation is 04-ui-ux.md's, driven by 10-turn-model.md §15; the platform supplies the stream and nothing about its pacing.
no-test-needed: it obliges a service that does not exist — there is no durable job queue to hold Cascades off the loop, and nothing under packages/server serves any observation stream over HTTP (only the live socket path exists) — so crash-recovery re-runs and retained compacted streams have no subject. The compaction itself stands pinned elsewhere: TM-2150's stream at packages/core/test/view/view-stream.test.ts. It becomes testable when §5 lands: a crashed resolution must then replay from the durable log to an identical result, and nothing partial may be persisted.
SVC-414 Absence MUST NOT be an exception path. A seat that never opens its Orders phase MUST resolve exactly as 10-turn-model.md and 13-command.md specify — persisted order stacks and Stances execute normally (TM-530), units with no Posture adopt their Fallback Posture for that turn only, and Dispatch items persist to that player's next session rather than being consumed (CM-2010). The platform MUST NOT invent a substitute behaviour of its own, and MUST NOT skip, freeze, or fast-forward an unattended seat. The service MUST additionally:
- maintain each position's
consecutiveMissedTurns(TM-570) as durable authoritative state; - run the substitution offer at the threshold — a vote of the remaining human players, or automatic
substitution in rated play (TM-580) — and record
SubstitutePlayerandResumePlayeras ordinary logged orders (TM-590, TM-600); - support adjournment (TM-620) as a participant- and operator-visible game state that halts at a turn boundary and resumes from the identical state and log;
- record every substitution and resumption in the game-history record (SVC-648).
Rationale: this is what makes correspondence play humane, and it is a genuine property of the turn model rather than a courtesy the platform grants. Because orders are persistent conditional policies evaluated at activation (13-command.md), a player who misses a turn has their standing plan carried out rather than their army frozen. The platform's job is to not get in the way of that: no forfeits, no skipped seats, no invented defaults.
no-test-needed: its engine half stands pinned in core — packages/core/test/net/stall.test.ts titles MP-1030 (an absent seat's army still executes its standing orders, on the same schedule either direction, with no rule module naming connection state) — but every service-side clause of this row waits on machinery not built: there is no durable consecutiveMissedTurns state anywhere under packages/server, no substitution vote to run or record as logged orders (TM-590/TM-600's surface), and no adjournment as a game state.
SVC-420 Joining MUST be by invite: either a capability-token link or a short human-typable code (8 characters, case-insensitive). The server assigns and tracks positions; players MUST NOT be required to enter a position number.
Rationale: a hand-typed position number is a data-entry hazard with no compensating benefit — a mistyped digit puts a player in the wrong seat of the right game, which is the most confusing failure the system can produce. A capability token identifies both the game and the seat and can be revoked.
Partially held 2026-09-28: joining by code stands strongly pinned; the capability-token half waits on identity machinery this tree does not have. The code disjunct of this invite holds today and is pinned thickly: packages/server/test/lobby-transport.test.ts titles MP-470's codes (presented, refused in the transport vocabulary, retired on regeneration), lobby-auto.test.ts launches a game from two code joiners over real sockets, and no surface in the client asks for a position number — seats are assigned as this row requires. What stands nowhere yet is the other disjunct: nothing under packages/server mints or tracks a capability token (there are no accounts to bind one to, §2.3), so link-based invitation waits with them.
SVC-430 Game start MUST NOT require a bootstrap circulation round: no file has to visit every player before the map can exist, and every join happens through the service rather than by forwarding a game file. Start timing is owned by 05-multiplayer.md MP-660 — by default the map is generated and turn 1 begins when all human seats are claimed, and the creator MAY instead start immediately with unclaimed seats held by stand-in AI until claimed. The platform MUST support both without a circulation round.
no-test-needed: it forbids bootstrap circulation for service-hosted starts, and the only start path in this tree never circulates anything — the pre-launch lobby generates its map on launch (packages/server/test/lobby-auto.test.ts titles MP-520's automatic launch over real sockets) with no file visiting any player. What waits is the account-gated half: seat claiming by identity, and both of the start-timing options that 05-multiplayer.md's MP-660 owns, ride on §2.3, which does not exist yet.
SVC-440 Retention: this document owns the async-game retention lifecycle; 05-multiplayer.md MP-720 states the same rule and cites this requirement. An async game MUST remain available while active. A stalled game (no orders from any seat) MUST first be nudged through §6 notifications, and is archived after a single server-configurable dormancy period — default 180 days, minimum 90 days (MP-720): an instance MAY raise the default but MUST NOT configure a period below the 90-day floor. All participants MUST be notified at least 30 days before archiving and again when it happens (SVC-510). Archiving is non-destructive: the game is frozen and delisted from active lists but retained in full — downloadable/exportable by any participant (SVC-190, MP-730) and revivable by any participant with one action (MP-720), which returns it to active status and restarts the dormancy clock. Hard deletion happens only on the unanimous request of the remaining participants (a shared game is no single player's to destroy) or under the instance's published data-retention policy (SVC-760/SVC-770), never by timeout.
Rationale: a game that silently expires destroys something several people spent months on, and the storage it reclaims is a few megabytes of order log. Making archiving non-destructive and one-click revivable reduces the cost of a wrong dormancy default from data loss to mild annoyance, which is what lets the default be a guess at all (open question 8).
no-test-needed: it owns a retention lifecycle for games no instance can yet lose — nothing under packages/server retains any game past its process (src/floor-seat.ts records the decision), so dormancy archiving, pre-archive notification and one-action revival have no subject. Its twin rule in 05-multiplayer.md's MP-720 carries the same decision on its own row there; the shared wait is §5's durable store.
SVC-445 Recovery is an audited server action: any state repair (rollback to an order index, substitute player, remove player) MUST be performed through the service with an audit record visible to all participants.
no-test-needed: it makes recovery an audited service action, and there is no service to act — nothing under packages/server performs or records any state repair (rollback, substitution or removal), because its games die with the process and there is no audit log to append a RollbackExecuted record to. It becomes testable when §5's store lands: every repair must then be participant-visible in the game's history forever.
SVC-448 Turn-bundle drop-box: the @everylastcity/server image MUST implement the drop-box bridge
that 05-multiplayer.md requires of it (MP-1190). An operator MAY switch it off for their instance
(MP-880 makes offering it optional) and the instance MUST then say so in /api/meta; the official
instance MUST offer it. Under the Orders/Cascade structure the drop-box is a collection point for one
turn's order bundles, not a baton passed between seats. Platform obligations where it is enabled:
- Scope. The drop-box stores and hands over signed order bundles keyed by
(GameId, turn, seat). The service MUST NOT simulate, validate, or rewrite a bundle's contents: it reads only the bundle header (GameId, turn number, issuing seat, content hashes) and treats the payload as opaque, so the game keeps bundle-mode trust semantics (signatures, no server simulation, MP-880). It is a plain HTTP path (SVC-404): upload, download, and collection MUST NOT require a live room or any other participant to be online. - Collection, not possession. The service MUST track which seats' bundles for the current turn have arrived, MUST accept a replacement bundle from the same seat for the same turn until that turn's set is declared complete (mirroring TM-360's free revision) and retain only the latest, and MUST reject a bundle for a turn already complete by returning the conflict to the client rather than overwriting it. Rollback stays the participants' explicit, co-ordinated act (MP-860).
- Every participant resolves the same Cascade. Once a turn's bundle set is complete, every participant holds the identical committed order set and computes the identical Cascade locally. The service MUST NOT be required to arbitrate the result, and download MUST be available to every participant with no one-download limit (MP-870).
- Notifications. A completed turn set MUST emit the orders-open event (SVC-510) for the next turn to every participant, and an incomplete set past the participants' agreed cadence MUST emit a reminder to the seats still outstanding. Drop-box games carry reminder notifications only — no enforceable timers or auto-turns (MP-1040) — so the stall escalations of SVC-560/MP-990 do not apply.
- Storage and quota. Bundle storage is instance-side game storage charged to the game, not to any participant's personal sync quota (SVC-360). The service MUST retain the current turn's set plus an instance-configurable number of complete predecessor turns (default 3) per game, subject to the instance's storage paths/quotas configuration (SVC-810) and the API rate limits of SVC-490.
- Retention. Drop-box games follow the SVC-440 lifecycle. Because every participant already holds the bundle chain locally (MP-730), archiving a drop-box game MUST NOT be treated as data loss.
- Access. Using the drop-box is server-hosted participation and therefore requires an account or an instance guest identity on that instance (SVC-020(b), SVC-167); the same game MUST remain fully playable by plain file hand-off with no account at all (MP-080, MP-840), and one action MUST convert it to full cloud async (MP-880).
Rationale, and it is why file-based play is worth keeping rather than a legacy obligation. Bundle mode is the only mode that needs no server, no account and no trust in an operator, which makes it the structural guarantee behind SVC-870 and pillar #5. The turn model improves it substantially: under sequential turn-taking a file had to visit all six players in order before turn N could end, so one slow participant blocked everyone. Here the six bundles are independent and arrive in any order, which collapses a round-robin into a single round per turn. That is a real gain, and it is the reason the drop-box is specified as a collector rather than as a mailbox holding one game file.
Contract, recorded rather than paraphrased: 05-multiplayer.md §10 still describes bundle play as a sequential possession chain with a single active seat (MP-830, MP-850, MP-860, MP-900). Those requirements MUST be respecified for the Orders/Cascade structure; this requirement states what the platform provides and does not attempt to fix them from here.
no-test-needed: the bridge it requires sits on surfaces this tree does not have — packages/server has no HTTP path to store or hand over bundles (only WebSockets plus /healthz), and 05-multiplayer.md §10 records its own flagged gaps about where bundle play's client half stands. What does stand is the container a bundle would carry: 03-architecture.md's save representation pinned at packages/client/test/save-container.test.ts under AR-070. It becomes testable when both ends land: upload, collection and download must then work with no room and no other participant online.
5.3 Workshop and sharing
Package formats, manifests, and client-side install behaviour are owned by 07-modding-content.md. This section specifies the hosting service.
SVC-450 The workshop MUST host every content kind 07-modding-content.md defines, in one service, matching MOD-820 and the manifest content keys of MOD-040 exactly: maps, scenarios, unit sets, asset/graphics packs, localization packs, name lists, rule presets, AI plugins, and worldgen (map-generator) plugins. Saved setup, map-generator-option, AI-option and victory Terms presets are not workshop content in v1 — they are profile data that syncs per SVC-320 and travels in the export (SVC-190); publishing them would require a package content key and format in 07 (MOD-040), which does not exist (see open question 10).
no-test-needed: there is no workshop anywhere in this tree to host anything — nothing under packages/server lists, stores or serves a single content item (its only game content is the two committed example configurations) — so kind-parity with 07-modding-content.md's catalogue has no subject. It becomes testable when §5.3 lands: every content key MOD-820 names must then be publishable through this one service, and presets must stay out per SVC-320.
SVC-460 Each workshop item MUST have: a stable item UUID; the package id (manifest.json id —
reverse-DNS or UUID, MOD-030) and the sim hash (MOD-140/MOD-160) of the package it publishes; an owning
publisher account; a display name and description; content-type; searchable tags; auto-extracted
metadata from the package manifest (for maps and scenarios: dimensions, topology, seat count,
Region count M and Landmark count L, the unit-set and terrain-set dependencies as (id, version range) pairs, and the victory Terms the scenario sets); explicit semantic versions with changelogs;
declared dependencies resolvable at install time; and optional screenshots/thumbnails.
The extracted set is defined by reference, not by restatement: it MUST be exactly the embedded metadata MOD-810 requires a map or scenario manifest to carry, plus the structural facts MOD-830 requires the publish flow to extract automatically. This document MUST NOT name a field the manifest format does not embed, and a field named here that extraction cannot find MUST fail the publish with a named error rather than index as empty.
One field named above is not yet covered on that side, and is recorded rather than papered over: a scenario's victory Terms are authored data that MOD-1080 lets a scenario set and MOD-800 validates against, but neither MOD-810's embedded-metadata list nor MOD-830's extraction list names them. 07-modding-content.md owes either that field in MOD-810's embedded metadata or an explicit statement that scenario Terms are not structurally indexable. Until it states one, the workshop MUST index Terms only where the manifest embeds them and MUST NOT fall back to asking the publisher to retype them.
Topology in particular MUST be stored, indexed, and offered as a browse filter as GR-190's three-value
enum — flat, cylinder, torus — and MUST NOT be re-encoded as a pair of independent per-axis wrap
booleans anywhere in the workshop's storage, index, or query surface.
Rationale: every one of those facts is machine-extractable from the manifest, and a service that does
not extract them forces publishers to encode them in the display name, where they cannot be filtered on,
cannot be validated, and go stale the moment the package changes. Versions with changelogs rather than
overwrite-in-place matter for the same reason they matter in any package registry: a game in progress
pins a version (MOD-160), and a publisher who can silently replace it breaks that game. Defining the set
by reference is what stops this requirement drifting away from the format it extracts from: an
un-updated list here does not fail, it silently indexes a field nobody writes any more, and the
publisher is back to encoding it in the name. The topology clause is the same argument at value level —
three named topologies do not round-trip through two booleans, so a boolean pair cannot express flat
and cylinder and torus without one of them being a coincidence of encoding. MOD-640 refused that
representation at the worldgen interface for exactly this reason; a workshop index that reintroduced it
would make "wrapping maps only" a filter that returns the wrong maps.
no-test-needed: it is the metadata contract for workshop listings that do not exist — there are no item UUIDs, publisher accounts or extraction pipelines anywhere in this tree (nothing under packages/server indexes a single package) — so none of the named fields can be checked. Its by-reference discipline binds whatever lands later: this document must not name a field the manifest format does not embed, and one it names that extraction cannot find must fail publish with a named error.
SVC-462 Item UUID vs. package id: the item UUID is a service-local handle for a listing on one
instance (listings, moderation, comments, subscriptions); the package id is the content identity that
clients, saves, lobbies, and hashes speak (MOD-030, MOD-160). The service MUST therefore: resolve
downloads by (package id, version) as well as by item UUID; keep the package id immutable for the life
of an item and refuse a new version whose manifest id differs; enforce one owning account per package id
per instance (MOD-910); and never require a package id to be listed in the workshop at all — sideloaded
(MOD-900) and server-served (MOD-870) packages are first-class. Item UUIDs are not portable between
instances (SVC-980); package ids are.
no-test-needed: it separates two identifiers neither exists in service form — no item UUID is minted anywhere (there are no listings) and package-id resolution has no registry to resolve against. What stands is the client side it points at: 07-modding-content.md owns what a package id is and how sideloaded packages stay first-class, under its own rows. It becomes testable when §5.3 lands: downloads must then work by (package id, version) as well as item UUID, and a changed manifest id must be refused.
SVC-465 Browsing, searching, and downloading public items MUST work anonymously and via a plain web page per item. Publishing, rating, and commenting require an account. Anonymous downloads MAY be rate-limited.
no-test-needed: it partitions a service between anonymous and account access, and no such service exists — there is no browse page, search or download endpoint anywhere under packages/server (its only non-socket surface is /healthz), so the anonymity guarantee has no subject. It becomes testable when §5.3 lands: browsing, searching and downloading public items must then succeed with no identity at all.
SVC-470 Subscription state ("what I have installed, which items have updates") MUST be tracked server-side per account and synced. Update notifications flow through §6.
Rationale: client-side install tracking breaks the moment a file is moved or a device is replaced, and the failure is silent — the player simply stops being told about updates. Server-side state also makes the lobby's content resolution (SVC-475) a lookup rather than a scan.
no-test-needed: it tracks subscriptions server-side per account, and there are neither accounts nor subscriptions in this tree — nothing under packages/server records what any device has installed or which items have updates. It becomes testable when §5.3's workshop lands: an update on a subscribed item must then reach the owner through §6 rather than by client scan.
SVC-475 In a multiplayer lobby, required content MUST be resolvable automatically. The lobby advertises (package id, version, sim hash) for every sim-affecting package, exactly as MOD-160 requires; clients verify their local sim hashes and fetch anything missing before launch, and a mismatch blocks launch (MOD-160, MP-110/MP-120). The workshop is one resolution source: a client MAY equally satisfy the requirement from already-installed or sideloaded content (MOD-900) or from the hosting server's own content service (MOD-870), so lobby content need not be published in any workshop. Whatever the source, the fetched package MUST match the advertised sim hash. No manual file copying, and no lobby ever keys content to a workshop item UUID (SVC-462).
no-test-needed: it makes lobby content resolution automatic, and the deployed lobby resolves no content at all — its committed example games run vanilla (packages/server/examples/) and no surface advertises package ids, versions or sim hashes. The check machinery it would use is 07-modding-content.md's to own (MOD-160 defines what a client verifies), cited rather than restated here; the lobby half waits with §5.3 and §2's account machinery.
5.4 Moderation and abuse
SVC-480 Every workshop item and public profile MUST carry a report action. Reports enter a staff review queue; outcomes are: no action, item unlisted, item removed, publisher warned, publisher suspended. A takedown process for IP claims MUST exist (DMCA-compliant for the official US-hosted instance).
no-test-needed: there is nothing in this tree to report — no workshop items exist (SVC-450's surface), no public profiles do either (profiles are unbuilt, SVC-100) and no staff queue or takedown process rides on any of it. It becomes testable when §2.3 and §5.3 land: every listed item and profile must then carry a working report action with the named outcomes.
SVC-485 Uploaded packages MUST pass the automated validation defined in 07-modding-content.md (schema, size caps, WASM sandbox constraints) before listing. The service MUST run no uploaded code outside the 07 sandbox.
no-test-needed: it gates uploads that do not exist — no upload path anywhere under packages/server (only WebSockets plus /healthz), so pre-listing validation has nothing to validate, and the WASM sandbox its second clause names does not appear in this tree at all. The format side stands by 07-modding-content.md's own rows, which define the schema and sandbox constraints; when uploads land, refusing a failing package with a named error is what this row checks.
SVC-490 All API surfaces MUST be rate-limited per token/IP with 429 responses; upload size limits are defined per content type in 07-modding-content.md, with a default per-account publisher quota of 2 GB (instance-configurable).
no-test-needed: it rate-limits API surfaces that do not exist — packages/server has no HTTP endpoint to answer 429 from (its only non-socket surface is /healthz), and its socket admission is gated by join code alone (MP-470) with no per-token or per-IP budget anywhere. It becomes testable when §5.1's API lands: every mutation must then refuse under saturation explicitly rather than fail unbounded.
6. Notifications
SVC-500 The service MUST implement notification fan-out with five channels: (a) Web Push (VAPID) — every instance MUST support it; it is the required non-proprietary baseline for browser clients and for any desktop client that speaks the Web Push protocol, and the one push channel a self-hosted operator can run without a third-party developer account (MP-980); (b) mobile push via APNs and FCM for the native iOS/Android clients; (c) desktop notification via the connected client's real-time channel — available only while that client holds a live room connection (SVC-403) — surfaced with native OS notifications; (d) email; (e) an authoritative in-app inbox that always receives every event. Where no push channel is available to a client, that client MUST fall back to polling its game list (MP-980); the in-app inbox (e) is never optional.
no-test-needed: no notification channel of any kind exists — nothing under packages/server references VAPID, APNs, FCM or SMTP (its manifest declares exactly one external runtime dependency, ws), and the client's only notification touch is a deep-link adapter docblock (UX-018) with no dispatcher behind it; what runs today are two WebSockets plus /healthz, which carry game traffic. Its clause (c) additionally rides on live room connections that do not exist yet (see SVC-403's own row), and the in-app inbox (e) has no storage to receive into. It becomes testable when any channel lands: each of the five must then be pinned under its own id, with the inbox receiving every event as the one never-optional floor.
SVC-510 Minimum event set:
| Event | Meaning | Source |
|---|---|---|
orders-open |
The Orders phase for turn N is open to you | TM-020, SVC-411 |
cascade-resolved |
The turn resolved; your observation stream is ready to review | TM-2150, SVC-413 |
deadline-approaching |
Your deadline is near and your reserve will begin to draw down | TM-510, SVC-412 |
reserve-low / reserve-exhausted |
Your reserve bank is nearly or fully spent in this game | TM-510, TM-520 |
attention |
A non-adaptable Attention Event on your Dispatch, where 14-victory.md publishes it to you — VictoryAlarm, VerdictOffered (VC-3150, VC-2440) |
CM-1720, CM-2010 |
substitution-vote |
A seat has missed the threshold; you are asked to vote | TM-580 |
substituted / resumed |
A seat was handed to an AI substitute, or reclaimed | TM-590, TM-600 |
game-invite / game-started |
Invitation received; the game has begun | MP-660, SVC-420 |
game-finished |
With your placing, final Standing and Tenure, and the winning route | VC-3030, VC-3050 |
dormancy-warning / archived |
Advance notice and the archive notice | SVC-440 |
bundle-turn-complete |
Every seat's bundle for the turn has arrived | SVC-448 |
workshop-updated / workshop-comment |
A subscribed item changed; a published item was commented on | SVC-470 |
security |
New-device sign-in — email mandatory | SVC-180 |
maintenance |
Service maintenance affecting active games | SVC-900 |
An attention notification MUST carry no fact its recipient could not already observe: Attention Events
are generated per player from that player's fog-filtered view alone (TM-2210, CM-1850), and the
notification MUST NOT widen them.
no-test-needed: this table names events whose engine-side sources are real but that nothing dispatches — packages/host-shared/test/deadline.test.ts pins the deadline, reserve and missed-turn chain (TM-490 through TM-610), so substitution-vote and reserve rows describe behaviour that actually happens server-side today; there is simply no fan-out to carry them (see SVC-500's own row), and the attention non-widening clause has no notifier it could catch widening. It becomes testable with fan-out: every event row maps to a pinned dispatch, and an attention notification must show nothing its recipient could not already see under TM-2210's per-player filtering.
SVC-520 Latency: orders-open SHOULD be dispatched within 60 seconds (p95) of the Cascade completing
and the next Orders phase opening.
Rationale: in a live game 60 seconds is the difference between a player noticing and a player wandering off; in a correspondence game it costs nothing and there is no reason to be slower. This is the number SVC-900's operational target is set against.
no-test-needed: a latency target for a dispatcher that does not exist — the Orders phase opens on engine mechanics (TM-400 pinned at packages/core/test/orders-phase.test.ts), but nothing in this tree emits an orders-open notification, so there is no dispatch timestamp to measure against. It becomes testable when fan-out lands per SVC-520's own clause: dispatch within 60 seconds p95 of the Cascade completing and the phase opening, measured on the server side where both facts are recorded.
SVC-530 Preferences MUST be per-profile and overridable per game: channel on/off per event type,
quiet hours in the profile's time zone, and a per-game mute. Defaults: orders-open → push + desktop,
with email fallback only if the seat has not committed for 24 h; attention and substitution-vote →
push + email, and MUST NOT be suppressible by quiet hours in a game with a deadline; invites → push +
email; security → email always.
Rationale: quiet hours are for convenience notifications. An event that will resolve without you — a Verdict vote that counts abstention as "play on" (VC-2770), or a rival two turns from winning — is exactly the class 13-command.md exempts from adaptive demotion (CM-1790), and the same reasoning applies to a notification channel as applies to the Dispatch.
no-test-needed: there is no notification preference to configure — the client holds exactly one local settings document (packages/client/src/settings/model.ts), which carries UI and dispatch tuning with no channel on/off keys, quiet hours or per-game mute; profiles themselves do not exist either (see SVC-100's own row). It becomes testable when both land: the defaults in this table must hold out of the box — orders-open on push plus desktop, security email always — and quiet-hours suppression must refuse to silence attention and substitution-vote where a deadline is live.
SVC-540 Service-originated email MUST be notification-only: the service MUST NOT attach game data (turn bundles, saves, order logs) to any message it sends — links into the client or web view only. This constrains the service, not the player: a player exporting a turn bundle and sending it from their own mail client or share sheet (MP-840) is explicitly unaffected and MUST remain possible with no account and no service involvement (MP-080). A daily digest mode MUST be offered that coalesces all open-orders, Dispatch-backlog and workshop events into one message. Every notification email MUST carry a one-click unsubscribe honouring CAN-SPAM/GDPR; transactional security email is exempt from unsubscribe.
no-test-needed: service-originated email cannot exist here because no sender exists — nothing under packages/server reads mail configuration (zero process.env references in its source) and imports no SMTP client, so there is no message to constrain, digest or unsubscribe from. The player-side clause is unaffected and already half-real: MP-840's bundle append-order machinery stands pinned at packages/host-shared/test/deadline.test.ts, independent of any mail path. It becomes testable when email lands: every outgoing message must pass one notification-only choke point — links only, no game data attached — with the daily digest and one-click unsubscribe pinned as its own behaviour.
SVC-550 Device push tokens MUST be stored per device record, refreshed by the client, and purged when the provider reports them invalid or the device is signed out.
no-test-needed: there are no device records to hold tokens in — server identity is join-code presentation alone (SVC-170's own row records that nothing anywhere issues or revokes a token), so storing, refreshing and purging push tokens per device has no subject. It becomes testable with SVC-165's sign-in machinery: each linked device record must then carry its provider tokens, refresh them on the client's schedule, and purge them on invalid-report or sign-out without touching any other device.
SVC-560 Vacation mode: a player MAY set an away window. The away window and the stall policy's vacation allowance (MP-990) are one mechanism, not two: while the window is in effect, the service MUST suspend that player's deadline and reserve draw-down (SVC-412) in every server-hosted game whose stall policy grants a vacation pool, drawing down that game's per-player pool (default 30 days/year, MP-990) one day per calendar day away, and MUST announce the away state to each affected game's participants. Days beyond an exhausted pool MUST NOT suspend that game's timers — the timer resumes, the player is warned before the pool runs out, and normal expiry actions (MP-990/MP-1000, TM-520) apply from then on. Reminder suppression applies for the whole window regardless of the pool, and in games with no deadline (the default for solo, two-player and hotseat games, TM-490) the window is purely a courtesy announcement. Pools are per game, so an away window may exhaust one game's pool while another still has days left. Bundle and drop-box games have no enforceable timers (MP-1040), so the window only suppresses their reminders.
An away window MUST NOT halt a game. The absent seat's turns resolve normally under SVC-414 — persisted
orders execute, and consecutiveMissedTurns accrues toward the substitution threshold exactly as it does
for any other absence (TM-570, TM-610).
Rationale: vacation mode buys a player relief from the clock, not the right to freeze five other people's game for a fortnight. The turn model makes that separation cheap, because an unattended seat plays its standing orders rather than doing nothing.
no-test-needed: no away-window machinery exists to suspend anything — neither packages/server nor packages/client contains vacation, pool or announcement code (the deadline and reserve draw-down this row would pause is pinned at packages/host-shared/test/deadline.test.ts under TM-490 through TM-520 and simply runs on). The one-mechanism clause with MP-990's stall-policy allowance therefore has no surface on either side, as does the per-game 30-day pool. It becomes testable when a window setting lands: suspension of timers and reminders while a game's pool holds days, the pre-exhaustion warning, announcement to each affected game's participants, and normal expiry from TM-520 once the pool is spent.
7. Statistics, history, and leaderboards
14-victory.md computes and retains everything in this section; the platform stores it, aggregates it, and reports on it. No requirement here recomputes a victory value, and none may present one at a precision finer than the band published for it (VC-3170).
SVC-600 Every finished game MUST produce a game-history record on each participating profile, containing at minimum:
| Group | Contents |
|---|---|
| Identity | Game UUID; name; instance; authority mode (MP-340); rated or unrated |
| Versions | RulesVersion, EngineVersion + build digest, victoryLogicVersion (VC-3220), FormatVersion (AR-880) |
| Content | Unit set and terrain set by package id, version and sim hash (AR-150, MOD-160); whether every sim-affecting package was certified, and the "Modded" label where it was not (MOD-170) |
| Terms | The Terms preset name where one was used unmodified (VC-2120), the stored Terms paragraph verbatim (VC-2000), the enabled Track set with their parameters, and every dismissed setup-linter warning (VC-3050) |
| Map | Source (generator + seed, premade map, or scenario id), size, MAP_CITIES, Landmark and Region counts |
| Participants | Accounts, guest identities, or AI identities with plugin version and weight version (AI-670); every seat substitution with predecessor, successor, turn and reason (SVC-648) |
| Outcome | The winner and the route by which they won; every player's placing, final Standing, final Tenure, and the turn they left (VC-3030, VC-3050) |
| Narrative | The complete Chronicle, unredacted (VC-2420, VC-2450) |
| Shape | Turn count; wall-clock duration; per-seat total time in Orders; count of turns each seat committed, timed out, or was absent for |
| Series | The per-turn series of SVC-605 |
Records are append-only and included in the export (SVC-190).
no-test-needed: there is no history store to append to — packages/server persists nothing past process exit (the decision is recorded in src/floor-seat.ts) and the client keeps no per-profile game records; the engine already retains everything this table lists for the Board within a live session, but none of it survives one. It becomes testable when persistence lands: the finished-game writer must emit every column here — versions, Terms paragraph verbatim, substitutions, placing and route, the unredacted Chronicle (VC-2450) — append-only, and include it in whatever export SVC-190's own row awaits.
SVC-605 The per-turn series MUST be the retained Seal records themselves (VC-250), not a second computation. For every player at every Seal that is: Standing and each of its five components (VC-510), Tenure, Seatless/Broken/Reduced state, and for every enabled Track the raw progress, threshold, ratio, Vigil counter and visibility band. The platform MUST additionally record per turn, from the documents that own them: cities held and their Integration distribution (12-economy.md), effective Works per turn (VC-530), Manpower stock, army Works-cost in supply (VC-540), units commissioned and lost, and the count of Clashes resolved (11-combat.md).
Rationale: this is a far better dataset than a raw unit-and-city count, and it costs nothing to collect because 14-victory.md already retains it for the Board (VC-2400). Standing decomposed into its five components tells a reader whether an empire grew by conquest or by development; Tenure tells them who was ahead and for how long; Track progress tells them what everyone was actually trying to do. Deriving the series a second time would be a second implementation of the scoring model and would eventually disagree with the first.
no-test-needed: no per-turn series is stored anywhere because there is no store (see SVC-600's own row); what exists is exactly the source this row names — the retained Seal records VC-2400 keeps for the Board, which a writer could copy verbatim rather than recompute. It becomes testable with history: the series must equal those Seal record-for-record — Standing and its five components (VC-510), Tenure, Seatless/Broken/Reduced state, Track progress against thresholds — plus the per-turn figures read from the documents that own them, cities, Works, Manpower, Clash counts.
SVC-610 Aggregates (games played; win rate and mean placing by Terms preset, by ruleset, by unit set, by map class; distribution of winning Tracks; human-versus-AI splits; hours played) MUST be derived from the record set, never stored as the only copy — so merges (SVC-660) and recomputation are always possible.
no-test-needed: no aggregate is derived or stored in this tree — nothing computes win rates, placing distributions or hours (the only ladder-shaped hit in engine source is a docblock about fidelity), and there is no record set to derive from either. The derive-never-store-only MUST therefore holds vacuously today; it matters the moment data exists. It becomes testable when the record set lands: every aggregate here must be recomputable from raw records, which is also what makes SVC-660's merge and any future correction safe.
SVC-615 Route-of-victory reporting: the service MUST be able to report, for a given ruleset version and Terms configuration, the distribution of winning Tracks, the median and 20th/80th-percentile game length in turns against the estimate the Terms presented (VC-2030), the rate at which games ended by Capitulation, by the Verdict, by mathematical elimination and by the Horizon, and the rate at which a Reduced player recovered (VC-2910). Reports MUST be aggregate-only and MUST NOT identify a player.
Rationale: 14-victory.md builds its length estimate from a build-time calibration table and makes a promise to the player on the setup screen about how long an evening will take (VC-2030, VC-2070), with CI failing when the estimate drifts more than 25 percent (VC-3290). The calibration comes from self-play (§8); this requirement is the check against reality, and it is the one measurement only the platform can make. It also answers the question the design most needs answered — whether the Track catalogue is actually varied in practice or whether one Track wins nine games in ten.
no-test-needed: no reporting surface exists — nothing queries finished games because none are retained (see SVC-600's own row). The self-play instrument this check rides on does run today, but it is a different measurement: scripts/self-play/ feeds VC-3210's build-time length table, whereas this row is the reality check against shipped games that only a platform can make. It becomes testable when the record set lands: winning-Track distribution, game-length percentiles against the Terms estimate (VC-2030), ending rates by route, and Reduced recovery — aggregate-only, with no player identifiable from any report.
SVC-620 Hours played MUST be tracked locally for every profile, guest or linked. An opt-in "community hours" board on the official service MUST let linked accounts contribute their total to a public community counter and listing.
Decision, replacing a flag that asked this be checked against another game: the board counts a single number — total hours played, summed across every mode including solo — and it is opt-in, resettable only by deleting the account's contribution, and displayed as a leaderboard of handles plus a headline community total. It is deliberately not a skill measure and MUST NOT be presented next to the ladders of SVC-640; conflating time spent with ability is the failure mode of every such board.
no-test-needed: hours are not tracked anywhere — the client's single settings document has no playtime field, no per-profile counter exists (profiles themselves do not, see SVC-100's own row), and nothing under packages/server accepts a contribution to any board. The opt-in community board is doubly held: its local input does not exist yet either. It becomes testable when local hour tracking lands first — total across all modes per this decision note — and the public counter half with linked accounts, presented away from SVC-640's ladders as the row itself requires.
SVC-630 Multiple commander profiles are the sanctioned way to keep records separate, and a linked account MAY hold several whose statistics never commingle.
no-test-needed: the subject of this row cannot commingle because it does not exist yet — no profile-scoped statistics are kept at all (see SVC-600 and SVC-100's own rows), so separation holds vacuously and a linked account has nothing to hold several of. It becomes testable when per-profile records land: two commander profiles of one account must keep disjoint record sets, with no aggregate that mixes the two.
SVC-635 Ladder keys: a leaderboard MUST be keyed by exactly the tuple that determines what skill means in that game, and by nothing else:
- Ruleset major version — a rules change that alters play invalidates comparison (AR-880).
- Content class — either certified stock content (every sim-affecting package certified, MOD-170) or one named non-certified content set. A "Modded" game MUST NOT be rated on a stock ladder.
- Terms configuration class — one of the named presets of VC-2120 used unmodified, or the single catch-all class Custom Terms.
A ladder MUST NOT be keyed by a per-game Terms hash, by map, or by player count.
Rationale: this is the trade-off between comparability and fragmentation, and both extremes are bad. One global ladder rates a 40-turn Blitz against a 320-turn Long War as though they measured the same thing. A ladder per exact configuration gives almost every game a ladder of one, where a rating is meaningless and a first win is a first place. Three axes, with named presets as the unit, keeps each ladder populated while keeping the games on it recognisably the same game. Custom Terms is deliberately one bucket rather than many: players who build their own Terms are a small population and are better served by one ladder that has other people on it.
no-test-needed: there is no leaderboard in any form — no Glicko or ladder implementation anywhere under packages/ (the only ladder-shaped hit in engine source is a docblock about fidelity) — so keying by this exact three-axis tuple, and excluding per-game Terms hashes, maps and player counts, have nothing to bind. It becomes testable when ladders land: each axis must appear as an explicit key component, named presets of VC-2120 unmodified plus one Custom Terms bucket as the unit, and a Modded game (MOD-170) must be refused on stock keys.
SVC-640 Leaderboards: the official service MUST provide ladders keyed per SVC-635, using Glicko-2 (or an equivalent published system, open question 5). Only games that ran in authoritative mode with server-side simulation and fog enforcement are ratable (MP-340, MP-350, MP-1160), and the service MUST verify a game by headless re-simulation before it enters ratings (MP-1180). Relay-only games and turn-bundle games — including drop-box games (SVC-448) — are never ratable: they are unrated by construction and the UI says so (MP-340, MP-1160). Every other game defaults to unrated; ranked participation is opt-in per game at setup.
no-test-needed: no ladder exists to provide (see SVC-635's own row), no rating pipeline to gate it, and no headless re-simulation verification anywhere for MP-1180's admission check — so authoritative-only ratable games, opt-in ranking at setup, and the UI marking that says relay and drop-box games are never ratable all have nothing to stand on. It becomes testable when ladders land: a rated game must verify by re-simulation before entry, every other game defaults to unrated, and a drop-box game (SVC-448) must be refused with the reason shown.
SVC-642 Rating from placings: a finished game MUST enter ratings as the full placing order (VC-3030), decomposed into pairwise outcomes with explicit handling of shared placings, rather than as winner-versus-field. Final Tenure MUST NOT itself be fed to the rating system as a score; it is the quantity that produced the placing (VC-1520) and feeding both would double-count it.
Rationale: in a six-player game "did not win" describes five very different performances, and a rating system that cannot tell second from sixth throws away most of what the game measured. Placings are safe to rate here in a way they usually are not, because 14-victory.md guarantees a placing is non-decreasing in the number of Seals a player survived (VC-3040) — so rating on placings cannot create an incentive to quit early, which is the standard objection to it. The design priced quitting before this document arrived; this requirement simply declines to un-price it.
no-test-needed: there is no rating system to feed — see SVC-640's own row for the shared absence; placing decomposition is a rule about that future system's input, and nothing today consumes placings as ratings at all. The engine does already compute what it would consume: full placing order VC-3030 with its non-decreasing-in-Seals guarantee (VC-3040), which is precisely why rating on placings cannot reward quitting here. It becomes testable when ladders land: a finished multi-seat game enters as the complete placing order with shared-placing handling pinned, and final Tenure appears nowhere in the score path — feeding both would be double-counting it.
SVC-645 Competitive conduct, reports, and enforcement (this requirement discharges MP-1160's
delegation of "ratings, reporting, and enforcement policy" to this document): every rated game MUST carry
a player-conduct report action available to each participant during the game and for 30 days after it
finishes. Both player reports and the automated tamper signals raised by 05-multiplayer.md — repeated
impossible or illegal orders (MP-1130), log-integrity or desync failures (MP-1140/MP-1150), and failed
re-simulation verification (MP-1180) — MUST enter the same staff review queue as SVC-480, with the game's
order log attached as the dispute artifact (MP-1180: the log is the evidence). Outcomes are: no action;
result annulled and removed from ratings with affected rating changes reverted; time-boxed ranked-play
suspension; account suspension (SVC-480). An automated signal MUST NOT by itself impose a penalty — a
human review decision is required — and every outcome that changes a player's rating or standing MUST be
shown to that player with its reason and an appeal path. A TurnTimeout (TM-520) and an unattended turn
(SVC-414) are not conduct signals and MUST NOT enter this queue. Self-hosted instances run their own
queue and outcomes (SVC-670, SVC-820); nothing here is gated to the official instance.
no-test-needed: no report queue, staff review or enforcement surface exists — packages/server's whole surface is its two WebSockets plus /healthz (see SVC-400's own row), so conduct reports, the shared queue with SVC-480, and rating-reverting outcomes have no target; neither do the automated signals this row would receive, since MP-1180's re-simulation verification is itself held. One boundary of the row does rest on live ground: timeouts and unattended turns are engine facts (TM-520 pinned at packages/host-shared/test/deadline.test.ts), which this row excludes from conduct — a distinction the future queue must preserve. It becomes testable when enforcement lands, with no-penalty-without-human-decision as its sharpest pin.
SVC-648 Substitution recording and rating eligibility (discharging MP-1020's "record the substitution for rating integrity"):
- The game-history record (SVC-600) and the game's rating entry MUST record every seat substitution — predecessor, successor, turn, and reason (missed-turn threshold, voluntary, admin action) — and every turn a seat was absent or timed out (TM-570).
- A timed-out or unattended turn MUST NOT by itself affect rateability. Under TM-530 the seat's own persisted orders execute; that is the player's plan being carried out, not a machine playing for them. Such turns MUST be counted in the record and MAY be shown on the ladder listing, but MUST NOT annul a result.
- A human→human substitution MUST leave the game ratable: each account is rated only on the turns it actually played, the replacement inherits the seat but never the predecessor's rating history (MP-1020), and both appear in the record.
- A seat handed to an AI substitute (TM-580, TM-590) MUST end rating eligibility for that seat from the substitution turn. If fewer than two human-played seats remain, the whole game MUST be recorded unrated. A seat reclaimed under TM-600 does not become ratable again for the turns the substitute played.
- Substitution MUST NOT alter the departed player's placing, which is recorded against them and computed from a Tenure that keeps accruing while the substitute holds the seat (VC-3060, VC-3070).
Rationale: the turn model changes what "played by someone else" means, and the rating rule has to follow it rather than the older sequential-turn intuition. Missing an Orders phase leaves your standing orders in command — a legitimate, if suboptimal, way to play a correspondence game, and penalising it would make the game hostile to exactly the audience correspondence play exists for. An AI substitute is different in kind: a different agent is now choosing, and rating the account for what it does would be rating the wrong player.
no-test-needed: no history record or rating entry exists to carry substitutions (see SVC-600 and SVC-640's own rows) — though the substitution machinery itself is real and pinned: packages/host-shared/test/deadline.test.ts holds TM-570 through TM-600, missed-turn escalation through AI substitutes and reclamation. Every clause here is therefore a rule about recording events that already happen but nothing records yet: timed-out turns counted without annulling rateability, human-to-human swaps keeping the game ratable per account, AI substitution ending eligibility from its turn and never restoring it. It becomes testable with history: the record must show predecessor, successor, turn and reason for each event drawn from that pinned machinery.
SVC-650 Leaderboard privacy: entries show the public handle only; a player MAY set "rate me but list me as Anonymous". Profile pages are private by default with per-section opt-in visibility.
no-test-needed: there are no public handles or profile pages to make anonymous (accounts are entirely held — see SVC-155's own row) and no ladder entries either (see SVC-640), so default-private visibility with per-section opt-in has no surface on which to stand. It becomes testable when both land, and is one pin: a player who opts into rate-but-anonymous appears on the ladder as Anonymous with nothing else attached, while every other profile section stays hidden until individually shared.
SVC-655 Match pages: a finished game MUST be able to produce a shareable match page rendering its end-of-game record (VC-3050) — the placings, the Terms paragraph verbatim, the Standing and Tenure series of SVC-605, and the complete unredacted Chronicle (VC-2450). Visibility MUST follow SVC-650 and MUST require the consent of every human participant before the page is public.
Rationale: the Chronicle is already a readable account of the game — VC-2460 shows an entire endgame in eight lines — and publishing it is the cheapest genuinely good thing the platform can do with the victory system. It is also the artifact a player wants after a six-month correspondence game, and the one thing that makes a long game's ending shareable.
no-test-needed: no shareable page can be produced — there is no retained end record to render (see SVC-600's own row), no visibility model to follow (SVC-650), and no participant-consent surface anywhere in the tree. It becomes testable when history lands: the page must carry exactly VC-3050's end-of-game record — placings, Terms paragraph verbatim, the SVC-605 series, the complete unredacted Chronicle (VC-2450) — and it must refuse publication until every human seat has consented.
SVC-660 Merging: importing a profile export (SVC-135/SVC-190) or linking a second device MUST merge history by union on game UUID — duplicates are impossible by construction, and merging is the reason SVC-610 forbids storing aggregates as the only copy.
no-test-needed: nothing merges today because nothing is stored to merge (see SVC-600's own row); union on game UUID is a property of the future store, not of any current code path — and profile exports are only partially held where SVC-190's own row says they are. It becomes testable when both export-import and history land: importing a duplicate must collapse to one record with no data loss, which is also what keeps SVC-610's derive-only aggregates honest.
SVC-670 Self-hosted instances keep their own statistics and ladders; ratings and records do not transfer between instances in v1 (SVC-980).
no-test-needed: the negative direction holds vacuously — there are no statistics or ladders on any instance (see SVC-640's own row) — but the positive claim, that self-hosted instances keep their own separate records and ratings, cannot be tested until this section's machinery exists to keep. It becomes testable when it lands: two independently run instances must show disjoint ratings and histories with nothing flowing between them in either direction.
8. The simulation farm and the AI pipeline
Three documents need large volumes of headless games and none of them owns the machinery: 06-ai.md's self-play tuning pipeline (AI-650 through AI-690), 03-architecture.md's nightly AI-vs-AI farm (AR-970), and 14-victory.md's build-time calibration and its CI gate (VC-3210, VC-3290). This section specifies what the platform provides them. It provides execution, records, and reproducibility; it does not provide experiment design, optimizer choice, promotion criteria, or benchmark content.
SVC-680 Boundary: this document MUST NOT define an experiment design, an optimizer, a promotion criterion, a benchmark suite, or a statistical threshold. Those belong to 06-ai.md (AI-650, AI-660, AI-720) and 14-victory.md (VC-3210). Where this section and an owning document both name a quantity, the owning document governs and this section is in error.
no-test-needed: this row constrains the document rather than code, and its subject holds by inspection — §8 names no experiment design, optimizer, promotion criterion, benchmark suite or statistical threshold, deferring every such quantity to its owning rows (AI-650, AI-660, AI-720; VC-3210) — and nothing in this tree implements a pipeline that could violate the boundary either: no candidate registry, no optimizer, and scripts/self-play/ is only VC-3210's length sampler. A text-level MUST has no executable pin by nature; it stands or falls at spec review on each revision of this section.
SVC-681 Batch execution: the platform MUST provide a farm job service that accepts an experiment
configuration plus a complete version pin set (SVC-682) and executes N headless games through
@everylastcity/cli farm (AR-810) on a worker pool, off any request or room loop (SVC-407). It MUST
support at least the batch sizes the owning documents require — AR-970's nightly ≥ 500 games, AI-660's
≥ 1,000-game promotion gate, and VC-3210's ≥ 2,000 games per calibration cell — and MUST report partial
progress and per-game exit codes (AR-820) rather than only a final verdict.
no-test-needed: no farm job service exists — nothing under packages/server accepts an experiment configuration and runs headless games on a worker pool, and the closest thing in the tree, scripts/self-play/farm.mjs, is VC-3210's length-table sampler, a different instrument by AR-970's own rationale. All three batch floors this row names are currently unmet at platform level: AR-970's nightly 500-plus games has no machine (its flagged gap of 2026-09-23 says so directly), and AI-660's promotion gate and VC-3210's calibration cells ride on the same wait. It becomes testable when a job service lands under the cli surface AR-810 names: batch size, partial progress reporting and per-game exit codes (AR-820) each need their own pin.
SVC-682 Reproducibility pinning: every farm job record MUST pin, and every result row MUST carry:
EngineVersion plus the core build digest (AR-300), RulesVersion, victoryLogicVersion (VC-3220),
every content package by id/version/sim hash, every AI plugin version with its weight version (AI-670),
and the PluginRuntimeConfig including the fuel transform id and version and which enforcement actually
applied (AR-857, AI-410). A job whose pins cannot all be resolved MUST fail rather than substitute a
nearest version.
Rationale: a farm result is evidence about a specific build, and evidence whose provenance is approximate is not evidence. The fuel transform is pinned for a reason 06-ai.md states directly: fuel counts are only comparable between hosts running the identical transform (AI-410), so a result produced under an unrecorded transform cannot be compared with anything.
no-test-needed: no job records exist to pin anything — see SVC-681's own row for the shared absence; a result row cannot carry provenance before its jobs run. The pin set this row enumerates does have live counterparts in the engine (AR-300 build digests, VC-3220 victoryLogicVersion, AR-857 and AI-410 fuel transforms) — they are simply never written to a farm record today. It becomes testable when job records land: every pin listed here must be recorded per game and carried onto each result row, with an unresolvable set failing the job outright rather than substituting a nearest version.
SVC-683 Result store: the platform MUST retain, per farm game, a queryable outcome row — placings, winning Track and route, final Standing and Tenure per seat, turn count, Chronicle class counts, per-seat fuel consumed, wall-clock cost, and exit code — indexed by every pin of SVC-682 and by the experiment and optimizer step that produced it.
no-test-needed: there is no result store to query (see SVC-681's own row) — no outcome rows, no index by pins or experiment step; what runs headless today reports verdicts to the terminal and retains nothing. It becomes testable when the store lands: one queryable row per farm game with every column named here — placings, winning Track and route, final Standing and Tenure per seat, fuel consumed, wall-clock cost, exit code — indexed by any combination of SVC-682's pins.
SVC-684 Artifact retention: for any farm game that crashed, diverged, stalled, violated a rules invariant, or regressed a budget, the platform MUST retain the minimized replay container AR-970 requires, for at least 90 days, and indefinitely while it is attached to an open defect. For a successful game the container MAY be discarded, but the seed and the full configuration MUST be retained so the game can be re-run exactly.
no-test-needed: no artifact retention exists because nothing runs in a place where artifacts would be kept (see SVC-681's own row); the minimized replay container AR-970 names as its required artifact is an engine capability with no 90-day or indefinite-retention policy around it. It becomes testable when the job service lands: any crashed, diverged, stalled, invariant-violating or budget-regressed game must retain its container at least 90 days and indefinitely under an open defect, while a successful game may discard the container only with seed and full configuration retained for exact re-run.
SVC-685 Candidate registry and promotion gates: the platform MUST hold AI weight and parameter candidates as versioned immutable artifacts with lineage (parent candidate, experiment, optimizer step), MUST be able to run a candidate against a named incumbent under a named gate, and MUST record the gate's verdict together with the statistics it rested on and the pins of SVC-682. The platform MUST NOT promote on its own. It computes and records; the criteria are AI-660's and the persona and tier-monotonicity regressions are AI-740's and AI-610's, and a promotion MUST be an explicit recorded action referencing a specific gate record.
Rationale: separating measurement from decision is what makes a promotion auditable six months later, and it keeps the platform from silently becoming the owner of the AI's balance. It also makes the failure mode visible: an automated promoter that quietly ships a candidate which passed one gate and regressed a persona test is very hard to notice, and very easy to prevent by requiring the decision to be a recorded act.
no-test-needed: no candidate registry, gate runner or promotion record exists — nothing under packages/ai holds versioned weight candidates with lineage (AIs are built from source each pass, and the ai-passive and ai-panel harnesses measure per change but retain nothing). The MUST-NOT-promote clause therefore stands vacuously today, which is exactly what must be preserved: when gates land, a promotion must remain an explicit recorded action referencing a specific gate record, never a side effect of a passing run. It becomes testable then, with that negative as its core pin — and the criteria staying AI-660's, not the platform's.
SVC-686 Rating pool: the farm MUST maintain a persistent rating pool over AI candidates, difficulty tiers and shipped releases, with frozen reference anchors that are never re-rated, so that a rating recorded six months ago still means what it said. The farm's rating namespace MUST be entirely separate from the player ladders of §7, and no rating MUST flow between them in either direction.
Rationale: a pool where everything is re-rated continuously drifts, and a drifting scale cannot answer the only question that matters — is this release stronger than the last one (AI-720). Anchors fix the scale. Keeping the namespaces apart is not fastidiousness: a player ladder rates humans under verification (SVC-640), the farm pool rates plugin builds under fuel metering, and a number that crossed between them would be meaningless in its destination.
no-test-needed: no rating pool exists on any farm (see SVC-681's own row) — hence no anchors to freeze — and no player ladders exist either (SVC-640), so the namespace-separation MUST holds vacuously on both sides with nothing able to drift. It becomes testable when the farm lands: a rating recorded months earlier must read identically against its frozen reference anchor, and no value may flow between this pool and §7's ladders in either direction.
SVC-687 Release gates: per release candidate, the platform MUST run and expose as CI-consumable results the release-to-release AI non-regression comparison AI-720 defines and the calibration regeneration VC-3210 defines, so that AR-960's determinism gate and VC-3290's estimate-drift gate have something to read.
no-test-needed: no CI-consumable release comparison exists — AR-960's determinism gate and VC-3290's estimate-drift gate currently have repository stand-ins (deterministic corpus replay under scripts/corpus-replay.mjs, the golden-hash pins, and scripts/self-play/ for calibration), none of which is a farm-produced release-to-release result, and the AI non-regression comparison AI-720 defines has no runner at all. It becomes testable when release-gate execution lands: each gate must read a machine result per release candidate with SVC-682's pins attached.
SVC-688 Determinism evidence: farm batches MUST be executable across the runtime matrix 03-architecture.md AR-960 defines, and a hash divergence between legs MUST be reported as an infrastructure or build failure, never as an AI result. A result produced with an unpinned or mismatched fuel transform MUST be marked not verification evidence (AI-410) rather than discarded, so the distinction survives into the record.
no-test-needed: no batch crosses the runtime matrix AR-960 defines to compare legs — there is no farm (see SVC-681's own row), and AR-970's flagged gap of 2026-09-23 names exactly this machine as missing; today's determinism evidence is single-runtime, from corpus replay against golden hashes. The not-verification-evidence marking for unpinned fuel transforms (AI-410) is likewise untestable until results carry transform provenance. It becomes testable when multi-leg execution lands: a hash divergence between legs must fail as infrastructure or build, never surface as an AI result.
SVC-689 Modder parity: the same farm interface — job submission, result store, artifact retention, and the rating pool — MUST be usable on self-hosted hardware against third-party AI plugins (AI-690), with no capability reserved to the official instance (SVC-040).
no-test-needed: there is no farm interface to make parity with (see SVC-681's own row) — and the substrate for self-hosting it on does not exist either, since no image ships (SVC-800), leaving operators a plain checkout. Modder access to job submission, results, artifact retention and the rating pool against third-party plugins (AI-690) is therefore held at the same machinery with nothing reserved yet because there is nothing to reserve. It becomes testable when the interface lands: an identical job must run on unmodified self-hosted hardware against a non-official plugin.
SVC-690 Isolation and cost control: farm workloads MUST run on capacity separate from player-serving capacity, MUST NOT contend with the request loop, the room server, or the Cascade workers of SVC-407, and MUST be subject to a per-job resource budget with a hard cap and a visible cost estimate before submission.
no-test-needed: no farm workloads exist to isolate — headless games in this tree run only as per-change harnesses (ai-passive, ai-panel) and scripts/self-play/ on developer machines, not as a pool; nothing under packages/server has worker-pool or budget machinery either. The capacity-separation MUST therefore stands vacuously until the farm exists at all. It becomes testable when it does: each job must show a hard resource cap with a cost estimate before submission, and player-serving loops — request handling, rooms, Cascade workers per SVC-407 — must stay unperturbed under concurrent farm load.
SVC-691 Farm inputs MUST be synthetic. The farm MUST generate its own games from configurations and seeds. Player games, player order logs, player observation streams, and telemetry MUST NOT be used as farm inputs, as tuning targets, or as training data. The telemetry consent of SVC-700 is not consent for this, and no requirement in this specification grants it; any future use of player data for AI training requires its own consent mechanism and its own spec revision (SVC-750).
Rationale: players did not agree to be training data, the balance telemetry of SVC-720 is deliberately aggregate and anonymous, and an AI tuned on real games would create an incentive to collect more of them than the privacy stance permits. Synthetic-only also keeps the farm reproducible, which player data never would be.
no-test-needed: the synthetic-only boundary stands by absence on both sides — no farm exists to consume inputs (see SVC-681's own row) and there is no telemetry or retained player data it could draw from either (SVC-700's own row), so none of the MUST-NOT clauses can fire. Its positive half already holds in miniature: every headless instrument in this tree — corpus replay, golden hash, self-play — generates its games from configurations and seeds alone. It becomes testable when the farm lands: no player-derived byte may reach job inputs, tuning targets or training data, and any future use requires the separate consent mechanism SVC-750 anticipates.
SVC-692 No training on player-serving infrastructure: 06-ai.md forbids shipped AIs from self-modifying weights during or between player games (AI-680). The platform MUST NOT execute a tuning or training job inside a player-serving instance's game path, and a self-hosted operator's labs mode MUST be explicit, separately enabled, and off by default.
no-test-needed: a serving instance's game path runs zero tuning jobs today — nothing under packages/server imports an AI training surface (its manifest declares exactly one external runtime dependency, ws), shipped AIs run read-only against fuel metering per AI-680, and no labs mode exists anywhere to enable. Both clauses therefore hold vacuously: there is no job that could run inside the game path, and a separately-enabled, default-off labs toggle is absent rather than mis-set. It becomes testable when either lands — a training job or a labs flag — with both explicitly enabled, off by default, and provably outside the player-serving instance's game path.
9. Telemetry, crash reporting, and privacy
SVC-700 Gameplay telemetry MUST be opt-in, default off, presented once at first run in plain language with equal-weight Yes/No choices. Consent state is stored per device + profile, changeable any time in settings, and recorded in the data export.
no-test-needed: no gameplay telemetry exists — no consent prompt, no consent state and no event pipeline under packages/client or packages/server (the only engine-source mention of telemetry is a docblock naming an instrument point for future use). The opt-in default-off MUST stands vacuously because nothing ever transmits; the positive half has no surface at all. It becomes testable when telemetry lands: first-run plain-language prompt with equal-weight choices, consent stored per device plus profile and changeable in settings, recorded in the export — with default off holding from day one as the clause this row exists for.
SVC-710 Telemetry MUST be anonymous: keyed to a random telemetry ID (player-resettable, one click), never to account UUID, handle, email, or hardware serials. Payloads MUST NOT contain free text typed by players, save contents, order logs, chat, or IP-derived location beyond country.
no-test-needed: nothing transmits, so nothing leaks (see SVC-700's own row) — no telemetry ID exists to reset and no payload channel exists to contain free text, save contents, order logs or chat; the client's settings document is persisted by its own service worker and never leaves the device. It becomes testable when telemetry lands: a captured payload must show only SVC-720's permitted categories keyed to a random one-click-resettable ID — no account UUID, handle, email or hardware serial — with scrubbing pinned per forbidden class named here.
SVC-720 Permitted telemetry event categories (exhaustive; additions require a spec revision):
| Category | Contents | Purpose |
|---|---|---|
| Session | app version, OS family, locale, device class | support matrix |
| Setup fingerprint | unit set key, ruleset version, Terms configuration class (SVC-635), map size and class, player count and types | which modes people actually play |
| Performance | frame-time p50/p95, Cascade resolution ms/turn, observation-stream size, memory class, map size bucket | the budgets of 03-architecture.md |
| Balance | per-game aggregate unit commissioning/loss/win counts by unit class | roster balance (02-units-and-industry.md) |
| Victory | winning Track, game length in turns against the Terms estimate, and counts of Capitulation, Verdict, mathematical elimination and Horizon endings | the calibration honesty check of SVC-615 and VC-3290 |
| Feature use | anonymous counters for named UI features | UX priorities (04-ui-ux.md) |
no-test-needed: this table regulates an absent pipeline (see SVC-700's own row) — client and server emit no events of any kind today. Two columns do have live counterparts the future emitter must align with: performance budgets measured against 03-architecture.md, and victory outcomes whose honesty check VC-3290 already runs over self-play samples rather than player data. It becomes testable when emission lands, as an exhaustive allowlist — any category outside this table failing outright is its sharpest pin, since additions require a spec revision by the row's own terms.
SVC-730 Crash reporting MUST be a separate consent (ask-at-crash allowed). With consent, a crash report MAY include stack traces, logs scrubbed of PII, and — with an explicit additional checkbox — the deterministic replay container (initial state + order log + seed, per 03-architecture.md) that reproduces the failure.
Rationale: a deterministic engine turns a bug report into a reproduction, which is the single largest support-cost reduction available to this project. It is also the most sensitive thing a player can send — a container is their whole game — which is why it is a separate, explicit, per-incident choice rather than part of the telemetry consent.
no-test-needed: no crash reporting exists — no handler uploads anywhere under packages/client, no ask-at-crash dialog and no separate consent state; with consent there is nothing to send and without it nothing to refuse. The replay-container attachment the extra checkbox would control is a real engine artifact (deterministic containers per 03-architecture.md), which is what makes this row's per-incident choice meaningful rather than ornamental when reporting lands. It becomes testable then: stack traces and scrubbed logs ride on crash consent, while the container requires its own explicit opt-in per incident — never implied by the telemetry consent.
SVC-740 No third-party analytics or advertising SDKs, ever, on any platform. Telemetry and crash
endpoints are first-party (the configured @everylastcity/server instance). Self-hosted servers and
their clients MUST default all telemetry off and MUST NOT transmit anything to the official service
(§10).
no-test-needed: holds by absence in both directions today — no third-party analytics or advertising SDK appears in any package manifest or source (the server's only external runtime dependency is ws), and a self-hosted client cannot contact anything official because no configured-server list exists at all: the session surface (packages/client/src/net/session.ts) opens exactly one url handed to it by its page. It becomes testable when any network-facing telemetry lands: first-party-only endpoints on the instance's own server, every self-hosted build defaulting off, and zero transmission toward the official service — a dependency audit plus an endpoint check.
SVC-750 A public privacy policy MUST be published (web + viewable in client), versioned in the repository, written in plain language, and enumerating exactly the data categories in this spec. Material changes require notice and fresh consent for affected processing.
no-test-needed: no privacy policy exists in this tree — nothing under docs/ is one, none of the site's 37 pages renders one (the only mention anywhere is the spec index line describing this document), and no client surface could display it. There is accordingly no versioned policy to change with notice or fresh consent. It becomes testable when the policy lands: a repository-versioned file, viewable in-client without an account, enumerating exactly this specification's data categories — after which material changes become a diff against the published version requiring notice.
SVC-760 Data-subject rights: machine-readable export (SVC-190) and account deletion MUST be self-service. Deletion completes within 30 days: PII erased; workshop publications either deleted or, at the owner's prior choice, anonymized-in-place; finished-game records of other participants are anonymized (the opponent becomes "Deleted player"), never removed from their histories. A 30-day grace period allows undo before purge. The service MUST NOT sell or share personal data with third parties for their own purposes.
no-test-needed: no accounts exist to delete, so that half has no surface at all; the export half is only partially held where SVC-190's own row says it is (the save-format container — nothing account-scoped yet), and workshop publications do not exist for their delete-or-anonymize choice to attach to. The MUST-NOT-sell clause stands vacuously with no data broker in the stack. It becomes testable in two stages: self-service machine-readable export when exports land, then deletion completing within 30 days when accounts do — PII erased, others' records anonymized as Deleted player rather than removed, and the 30-day undo window pinned before purge.
SVC-770 Server request logs MUST truncate/anonymize IP addresses within 30 days. Backups containing deleted-account data MUST age out within 90 days.
no-test-needed: there is no request logging to truncate — packages/server writes a handful of human console lines at game launch (src/main.ts), nothing structured, and no stored IPs; it also persists nothing past process exit (the decision is recorded in src/floor-seat.ts), so the 90-day backup-aging clause has no backups to reach either. It becomes testable when logging lands: request IDs with IP truncation inside 30 days become a retention pin on the logs themselves, and whichever stores hold deleted-account data must age them out within 90 days.
SVC-780 The project SHOULD publish an annual aggregate transparency post (player counts, takedown counts, telemetry aggregates) as the public face of the privacy stance.
no-test-needed: a SHOULD with nothing to report yet — no player counts, takedown queue or telemetry aggregates exist (the only operational surface is /healthz under SVC-920), and neither site/ nor docs/ publishes any operational numbers. The row binds the project's future public face rather than this tree: when the official instance runs real populations, an annual aggregate post covering player counts, takedown counts and telemetry aggregates satisfies it. It stays dormant by design until there is a population to count.
10. Self-hosting
SVC-800 Each release MUST publish a public, multi-arch (x64/arm64) Docker image of
@everylastcity/server, carrying the Node 22 LTS runtime, both network surfaces (SVC-401), and
@everylastcity/cli (03-architecture.md AR-800). docker run -p 8080:8080 -v elc:/data everylastcity/service MUST yield a working instance with zero configuration: a PostgreSQL server
bundled inside the image, initialized on first run into the mounted volume, a filesystem blob store in
that same volume, and a printed one-time admin bootstrap token. Setting a DATABASE_URL MUST make the
instance use that external database and MUST NOT start the bundled one, so a single image serves both the
one-command hobby deployment and a managed-Postgres production deployment; the official instance MUST
deploy this same image (SVC-040) against an external database.
Decision and its cost, stated rather than deferred: bundling the database makes this a multi-process
container and couples PostgreSQL major-version upgrades to image upgrades. That cost is accepted because
"one command, no prerequisites" is load-bearing for pillar #5 and because the alternative — a second
supported SQL engine — is worse (SVC-050). The obligation it creates is discharged by SVC-860: the image
MUST ship a documented, tested major-version upgrade path and expose pg_dump/pg_restore from inside
the container. Until that path is demonstrated against a volume holding a multi-year correspondence
game, this arrangement is a decision under test rather than a settled one (open question 12).
no-test-needed: no image exists to publish — there is no Dockerfile anywhere in this tree, and the official instance runs from a plain git checkout under systemd user services; the server itself reads zero process.env references in its source, so DATABASE_URL switching, the bundled PostgreSQL first-run initialization and the printed bootstrap token have nothing to bind. It becomes testable when packaging lands: docker run with the documented volume must yield a working instance as written here, and setting DATABASE_URL must verifiably suppress the bundled database — that last clause is the one protecting the multi-year correspondence game this decision note flags.
SVC-810 Configuration MUST be via environment variables and/or one mounted config file:
DATABASE_URL (absent ⇒ the bundled PostgreSQL of SVC-800), blob store (path or S3 endpoint/bucket/
credentials), public base URL, real-time room server enable/disable and its listen port (SVC-404 —
disabling it MUST leave every non-real-time capability intact), optional REDIS_URL for multi-process
room presence and matchmaking (SVC-407), SMTP relay for email (optional — without it, email features
degrade gracefully to in-app/push only), Web Push VAPID key pair and contact address (SVC-500,
MP-1200; auto-generated on first run if absent, so push works out of the box with no third-party
account), APNs/FCM credentials (optional), auth mode flags including the instance account policy
open/invite/guest (SVC-165, SVC-167, MP-1200), listen port/TLS and external URL (MP-1200), default stall
policies (MP-990) and default Orders deadlines and reserve banks for games created on the instance
(TM-480, TM-490), Cascade worker count and per-game CPU budget (SVC-407, SVC-413), farm worker pool size
and per-job budget cap (SVC-690), and retention/quota overrides (SVC-360, SVC-440 subject to its 90-day
dormancy floor, SVC-448, SVC-490).
no-test-needed: there is no configuration surface to configure — packages/server/src contains zero process.env reads (its config test, MP-1200 at packages/server/test/config-surface.test.ts, pins exactly what little it accepts today), so none of this row's knobs exist: no DATABASE_URL, blob store, VAPID key pair, SMTP relay, room enable/disable toggle, or farm pool size. It becomes testable when the surface lands: every variable named here must appear as a documented env var or mounted-config key with its absence default holding — bundled database on, rooms on unless disabled with every non-real-time capability intact, telemetry off.
SVC-820 Feature parity: async and correspondence games, the turn-bundle drop-box (SVC-448), real-time
relay and authoritative simulation (MP-1190), the workshop, sync, notifications, statistics and ladders,
the farm interface (SVC-689), and moderation and enforcement tooling (SVC-480, SVC-645) MUST all function
on a self-hosted instance. Operational exceptions are limited to: native mobile push to the
iOS/Android clients requires the operator's own APNs/FCM credentials; email requires an SMTP relay;
nothing else. Turning the real-time room server off (SVC-810) is an operator's deliberate choice
announced at /api/meta, not a capability the image lacks — the image MUST always be able to run it. Web
Push (SVC-500) requires no third-party account and therefore MUST work on every self-hosted instance out
of the box (MP-980), so an instance without APNs/FCM or SMTP still delivers push and in-app
notifications.
no-test-needed: the parity list names capabilities most of which do not exist yet — workshop, sync, notifications, statistics and ladders, the farm interface, moderation tooling are each held at their own rows in §4 through §9, and the packaging substrate is absent too (see SVC-800's own row). What works today — live games plus lobby over WebSocket from a plain checkout — would be the whole feature set of a self-hosted instance if it were packaged. It becomes testable per capability as each lands on the image: one parity matrix row per named feature, with native mobile push and email as the only permitted degradations behind missing credentials.
SVC-830 The client MUST support multiple configured servers (official plus any number of self-hosted URLs), with a per-server identity and a clear indicator of which server a game or workshop view belongs to. Accounts, content, and ratings are scoped to one server (no federation in v1, SVC-980).
no-test-needed: the client knows exactly one server — its session surface (packages/client/src/net/session.ts) opens a single url handed to it by the embedding page, the settings model carries no server list or per-server identity keys, and an indicator of which instance a view belongs to cannot exist when there is only ever one. It becomes testable when multi-instance lands: several configured URLs with per-server identity, accounts and ratings scoped to each, and a visible origin marker on every game and workshop view.
SVC-840 A self-hosted instance MUST NOT contact the official service or any third party as a side effect of normal operation. An update-availability check MAY exist but MUST default off.
no-test-needed: holds by construction today — the client's entire outbound surface is that one session url (packages/client/src/net/session.ts); no other network endpoint, update check or third-party host appears in any non-test source under packages/. A self-hosted instance therefore contacts nothing official and nothing else, which makes both halves of this row true for want of anything to violate them. It becomes testable when any new egress lands: an update-availability check is the single permitted exception and must default off — pinned that way if and when it exists.
SVC-850 Sizing guidance (SHOULD, informative targets): 2 vCPU / 2 GB RAM / 10 GB disk serves a group of ≤ 50 accounts with hundreds of concurrent correspondence games and a handful of live rooms; the async model is storage-light because games are order logs (03-architecture.md). Correspondence load is bursty rather than sustained — a game's whole CPU cost is one Cascade per turn, arriving when the last seat commits — so the binding constraint is peak concurrent Cascade resolution, not average utilization. 1 GB RAM remains workable only with an external database (SVC-800), no authoritative real-time games, and no farm workers on the same host (SVC-690).
no-test-needed: a sizing SHOULD with no measurement to stand on — no instance has produced a vCPU, RAM or disk profile (the official one runs from a checkout with no capacity instrumentation), nothing in the repository benchmarks service headroom, and the storage-light claim's premise, persisted order logs, does not exist yet (src/floor-seat.ts records why). It becomes testable once real traffic exists: each target here — 50 accounts on 2 vCPU / 2 GB / 10 GB, the external-database-only path to 1 GB RAM — becomes a load-profile regression check with peak concurrent Cascade resolution as its binding constraint.
SVC-860 Operator documentation MUST ship with the image: compose example, TLS guidance (reverse
proxy), backup/restore procedure (pg_dump + blob dir — the bundled database of SVC-800 MUST expose
pg_dump/pg_restore from inside the container so the one-command deployment is still backup-able), and
upgrade/migration notes including the PostgreSQL major-version path required by SVC-800. Database
migrations MUST be checked-in SQL files, MUST run automatically at startup, and MUST be reversible one
version back. Rationale: migrations generated at runtime from an ORM model are exactly the kind of
implicit behaviour that is unreviewable and untestable; plain SQL under version control is a diffable
artifact.
no-test-needed: no operator documentation ships because there is no image to document (see SVC-800's own row) — nothing under docs/ carries compose examples, reverse-proxy TLS guidance or a pg_dump backup procedure, and the migration clauses have no database at all in this tree. It becomes testable when packaging lands: every artifact named here must ship with the image — checked-in SQL migrations that run automatically at startup and reverse exactly one version back is a format choice pinnable from the first release.
SVC-870 Continuity guarantee: because the image, formats, and API are published, the official service shutting down MUST NOT strand players — see SVC-950. Server-to-server game export/import (a game archive any instance can ingest) MUST exist so a community can move a game off a dying instance.
Rationale: a correspondence game is designed to run for years (MP-630) and may easily outlive the company hosting it. A game people have played for eighteen months must not be destroyed by a hosting decision they had no part in, and the only real defence is that every instance runs the same published image over published formats — self-host parity is a structural guarantee rather than a promise.
no-test-needed: both halves are absent, so the guarantee has nothing to act on yet — no game archive exists to export or import (nothing persists past process exit; src/floor-seat.ts records the deliberate decision) and the sunset procedure it points at owns no tooling either (SVC-950). What does exist is a future substrate: the portable save representation pinned under AR-070 at packages/client/test/save-container.test.ts, which a server-to-server archive could ride on. It becomes testable when archives land: one instance must ingest another's exported game and continue it turn-for-turn without loss.
11. Operations (official instance)
SVC-900 Service level objectives are internal targets, not contractual SLAs:
| Metric | Target |
|---|---|
| API monthly availability | ≥ 99.5% |
Order submission and CommitOrders durable-ack latency (HTTP path, SVC-404) |
≤ 5 s p99 |
Cascade resolved after CloseOrders becomes durable (8 players, Standard-tier map) |
≤ 10 s p95 (SVC-413) |
| Room materialization for an existing game (SVC-403) | ≤ 2 s p95 |
| Orders-open → push dispatched | ≤ 60 s p95 (SVC-520) |
| Database RPO | ≤ 24 h (nightly backups) |
| Game order-log RPO | ~0 (synchronous replication / versioned bucket) |
| Full-disaster RTO | ≤ 72 h |
no-test-needed: internal SLOs with no measurement stack behind them — the only operational surface is /healthz (pinned under SVC-920 at packages/server/test/healthz.test.ts), whose implementation note in src/healthz.ts already records that Prometheus metrics are owed; there is no availability, latency or RPO instrumentation of any kind. It becomes testable when metrics land: every table row here becomes a queryable SLO check — the 60-second orders-open target sharing its number with SVC-520 first among them once fan-out exists to dispatch.
SVC-910 Backups: nightly database dumps and blob-store versioning MUST be enabled, retained ≥ 30 days (subject to SVC-770 aging for deleted accounts), stored off the serving infrastructure, and restore-tested at least quarterly.
no-test-needed: there is nothing to back up yet — no database, no blob store, no persistence past process exit (src/floor-seat.ts records the deliberate decision), so nightly dumps and blob versioning have no subject and a quarterly restore test has nothing to restore. It becomes testable in stages as durable state lands: first nightly dumps with 30-plus-day retention stored off serving infrastructure for whatever persists, then restore drills pinned at least quarterly — with SVC-770's aging rule governing what those dumps may keep about deleted accounts.
SVC-920 Observability: the service MUST expose /healthz (liveness/readiness) and Prometheus metrics;
logs MUST be structured (JSON) with request IDs and MUST NOT contain PII beyond what SVC-770 permits.
Readiness MUST report the HTTP surface and the room server as separate subsystems, and the HTTP
surface MUST report ready while the room server is unavailable — otherwise a room-server fault would take
async play down with it, contradicting SVC-404. Metrics MUST include live room count and occupancy,
event-loop lag, Cascade queue depth and resolution time per game size, farm queue depth (§8), and the
count of games whose Orders phase is open past its deadline. A public status page MUST report availability
and incidents, distinguishing a real-time outage from an async/API outage.
Implementation note, recorded 2026-09-24 (Codex CLI): the /healthz half pinned at packages/server/test/healthz.test.ts — both host modes answer liveness/readiness on their own serving port (an HTTP surface sharing each process’s single WebSocket port): GET /healthz answers ready with subsystems http and roomServer reported separately; v1 has no room server, so it stands absent, and the aggregate stays ready while it is unavailable — SVC-404’s non-coupling clause. Owed on this row: Prometheus metrics (live room count/occupancy et al.), structured JSON logs with request IDs bounded by SVC-770, the room server’s readiness entry when SVC-404 lands, and the public status page.
SVC-930 Security operations: dependencies scanned in CI; images signed; a published vulnerability-disclosure policy with a security contact; breach notification per applicable law. The service holds no payment data ever (SVC-960), keeping compliance surface minimal. The dependency graph MUST be pinned by a committed lockfile, installed with package lifecycle scripts disabled in CI and in the image build, and the published image MUST carry build provenance. Rationale: the npm ecosystem's transitive graph and install-time script execution are a materially large supply-chain surface; these are the mitigations, and they are a standing operational cost, not a one-time setup step.
no-test-needed: the supply-chain clause stands only in its lockfile half today — pnpm-lock.yaml is committed and pins the whole dependency graph, which is the row's first MUST; there is no CI dependency scan, no image to sign or attach build provenance to (see SVC-800's own row), no published disclosure policy under docs/, and no breach-notification process. It becomes testable as each part lands: a scanning gate in verify when it arrives, signing plus provenance claims with the first image — and lifecycle-scripts-disabled installation pinned wherever CI installs happen once images exist.
SVC-940 Client/server compatibility windows: the official service MUST support the current and previous major client API versions (SVC-400); clients older than that receive a clear must-update response, and in-progress correspondence games MUST remain completable during any deprecation window of at least 90 days.
no-test-needed: there is no versioned API to deprecate — packages/server has no /api/v1 surface at all (see SVC-400's own row), so compatibility windows, must-update responses and the correspondence-completable-during-deprecation clause have neither a client nor a server edge to hold. It becomes testable when versioned APIs land: current-plus-previous major support becomes an interop matrix row on both sides, with in-progress games completing through any deprecation window of at least 90 days pinned against real long-running state once persistence exists.
SVC-950 Sunset policy: if the official service is ever discontinued, the project MUST give ≥ 180 days notice, keep export (SVC-190) and game-archive migration (SVC-870) available through end-of-life, and publish a final service release plus migration guide so communities can continue on self-hosted instances. This is a standing commitment of pillar #5.
no-test-needed: a standing commitment with no mechanism yet — nothing in this tree is sunset tooling: no export surface for account data (see SVC-760's own row), no game-archive migration path (SVC-870), and no final-release process to publish a guide against. It binds future action rather than current code, so each clause verifies at trigger time instead of now — 180 days notice measured from the decision, export and archive migration available through end-of-life, and a published final release plus migration guide that keeps self-hosted communities running.
12. Non-goals (base spec)
SVC-960 No monetization inside the platform: no advertising, no ad SDKs, no microtransactions, no paid currencies, no loot boxes, no paid workshop marketplace, and no payment processing of any kind in the base spec. Any future paid offering (e.g., cosmetic DLC sold through platform stores) requires its own spec revision and MUST NOT touch gameplay or gate SVC-020 capabilities.
no-test-needed: holds by absence — no payment processing, ad SDK or commerce code appears anywhere in the tree; every package manifest is workspace siblings plus build tooling with the server's single external dependency ws, and the only payment-shaped hits in client source are docblocks about an in-game mechanic (EC-800). The spec-revision gate it sets for future paid offerings is a review-time check like SVC-680's boundary. It becomes testable as an audit when anything commerce-shaped lands: no dependency, endpoint or UI path may touch money without the revision this row requires.
SVC-970 No mandatory online: nothing in this spec may ever make an account, connectivity, or telemetry a requirement for the offline feature set (SVC-010). This clause wins over any other requirement in this document.
no-test-needed: the clause governs this document and everything built from it, and today's build obeys it by construction — every shipped surface runs locally or against an operator-chosen server (the client persists its entire settings model with no account at all), nothing in the tree makes connectivity mandatory, and there is no login to require one. As a spec-level prohibition it enforces at review time rather than under a test runner: any future requirement that gated offline play on accounts, connectivity or telemetry violates this row first, which is what its wins-over-everything wording exists to make visible.
SVC-980 No federation in v1: no cross-instance identity, matchmaking, rating transfer, or workshop
mirroring between @everylastcity/server instances. The API is designed not to preclude it later.
no-test-needed: holds by absence — no cross-instance identity, matchmaking, rating transfer or workshop mirroring exists because there are no accounts (SVC-150), no ladders (SVC-640) and no workshops at all; the client speaks to a single handed-in server url with no list to federate across (see SVC-830's own row). The designed-not-to-preclude clause is untestable by construction, so this row lives or dies entirely on what §5 through §7 build: it becomes an audit once any of those surfaces exists, checking that none of them reaches across instances.
SVC-990 No social platform: no friend feeds, DM system, or presence broadcasting beyond what lobbies, invites (05-multiplayer.md) and the Orders-phase presence of TM-440 require; no scraping-friendly public member directory; no email marketing (announcement newsletters only by separate double opt-in).
no-test-needed: holds by absence — no friend feeds, DM channels or presence broadcasting exist anywhere beyond lobby membership itself (the lobby surface is pinned under its own tests, packages/server/test/lobby.test.ts and kin, for their MP rows), no public member directory exists to scrape, and the email-marketing clause stands vacuously because the service sends no email at all (SVC-540's own row). It becomes an audit when any social surface lands: nothing may broadcast presence beyond what lobbies, invites and TM-440's Orders-phase presence require, and announcement mail stays behind its own double opt-in if it ever exists.
Open questions
- Server code licensing: is
@everylastcity/serveropen source or source-available? SVC-040/SVC-800 require the published image; the pillar-4 spirit suggests source, and this is a business decision — but the stack narrows it, since a Node image ships readable JavaScript and "binary-image-only" is no longer a meaningful third option. - Community hours board (SVC-620): is a single all-modes total the right number, or should solo hours be listed separately? A board dominated by solo hours measures something different from one dominated by correspondence hours, and the design has not decided which it wants to celebrate.
- Which OAuth providers ship at launch (Steam certainly for the Steam build; Apple mandatory on iOS if any others are present) and whether Steam identity can satisfy account creation with no email at all (impacts SVC-155's "optional verified email" and security notices SVC-180).
- Free-tier quota (SVC-360) and workshop publisher quota (SVC-490) final numbers once real container and package sizes exist from 03/07 implementations.
- Rating system choice (SVC-640, SVC-642). Glicko-2 with placings decomposed into pairwise outcomes is the current answer, but a rating system that consumes a full placing order natively — Weng–Lin / OpenSkill and its relatives — fits this game's shape better and would avoid the decomposition entirely. Decide before the first ladder opens, because ratings cannot be recomputed backwards across a system change without invalidating everyone's history. Team games remain a separate question (05-multiplayer.md dependency).
- Whether E2E-encrypted solo saves (SVC-350 MAY) make v1.0 or wait, given they complicate support and key handling. Every client is now a web client (the desktop and mobile builds wrap the same one), so browser key storage is the only case rather than an edge case, and a cleared browser origin means an unrecoverable save.
- Data residency: single-region hosting is assumed; is an EU region needed at launch for GDPR comfort, or is a documented processor list sufficient?
- Dormancy default (a single 180-day window with a 90-day floor and 30-day advance notice, SVC-440, MP-720): validate against real correspondence cadence. The design lifetime is months to years (MP-630), so the default may need to be longer; because archiving is non-destructive and one-click revivable, the cost of a short window is annoyance rather than loss. SVC-615's data will answer this directly once games exist.
- Official-instance guest policy (SVC-167): the default is guests off — do we ever enable them for invite-only real-time games, and if so how do guests interact with moderation, appeals, and report credibility (SVC-645)?
- Should saved setup, map-generator-option, AI-option and victory Terms configurations become shareable workshop content (SVC-450)? Terms configurations make the strongest case of the four — a good Terms setup is a designed thing that other people would want — but it requires a content key and package format in 07-modding-content.md (MOD-040) that does not exist, and it interacts with SVC-635's "Custom Terms" ladder bucket: a widely shared custom Terms configuration is arguably a preset and arguably deserves its own ladder.
- Reframed by the turn model; the old form is answered. The previous question asked how many auto-turns a rated seat may take before annulment. Under Orders and the Cascade a missed Orders phase is ordinary play — the seat's own persisted orders execute (TM-530) — so it is not an integrity event at all, and SVC-648 says so. What remains is the narrower question: a seat handed to an AI substitute loses rating eligibility from that turn, but how many substituted turns should annul the whole result rather than merely mark it? Needs ladder playtesting (cf. 05-multiplayer.md's annulment question).
- Is a bundled PostgreSQL inside the image the right zero-config answer (SVC-800)? The alternatives are a compose file as the blessed one-command path (which gives up "one image, one command" and with it some of SVC-040's simplicity) or an embedded WASM Postgres for tiny instances (which reintroduces a second engine's behaviour under the same dialect). The deciding factor is the major-version upgrade path for a volume that holds years-long correspondence games; nothing here is settled until that is demonstrated, and SVC-860 is where the demonstration lands.
- Colyseus dependency risk (SVC-402): the framework sits in the critical path for real-time play, and its blast radius is deliberately small — rooms, matchmaking, presence, lifecycle, with the protocol and all game state kept outside it. Should we cost a plain WebSocket room server behind the same MP-260 protocol as a fallback before committing, and what signal would trigger it?
- Deployment topology (SVC-401, SVC-407): does the official instance run the HTTP API, the Cascade workers and the room server as separately scaled deployments while self-hosts run the single image? SVC-040 requires the same code and image, not the same topology — that reading should be confirmed explicitly, or the official instance quietly drifts from what an operator can reproduce.
- Cascade capacity (SVC-413, SVC-850, SVC-900): the ≤ 10 s p95 target is an estimate, not a measurement, and it is the number the whole async service is sized against. How many concurrent 8-player Standard-tier Cascades does one worker sustain, and what does a Large-tier game cost? Measure against 03-architecture.md's reference fixture before SVC-850's sizing numbers are treated as guidance.
- Runtime lifecycle vs. game lifetime (SVC-940, MP-630): correspondence games are designed to run for years and will outlive Node 22's maintenance window. What is the runtime-upgrade commitment for an instance hosting live games, and MUST an image upgrade ever require a participant to do anything? The answer should be no, but it has not been designed.
- How HTTP entitlement is enforced in the API shape (SVC-404, SVC-190). That a fog-enforced seat gets
its compacted observation stream and an AR-500
ViewCheckpointrather than the log is settled (MP-250 binds every transport). What is not settled is the mechanism: does every game-scoped read endpoint re-evaluate entitlement from(GameId, seat, authority mode)on each request, or does the service mint a seat-scoped read capability at join time? The capability form is cheaper to audit but MUST be re-issued on an MP-905 authority-mode switch or it silently outlives the mode it was scoped to. This must be decided before the OpenAPI document (SVC-400) freezes, because the two answers give the game-fetch endpoint different response shapes. - When a fog-enforced game's log becomes fetchable over HTTP (SVC-404, SVC-440). MP-730 unlocks the full container only after the game ends (the MP-1100 replay) or through a consent-gated mode switch, so the service has an entitlement transition to implement: which server-side event flips it, and is a game that was archived while unfinished (SVC-440) still "live" for MP-730? It should be — archiving is non-destructive and one-click revivable, so treating it as an ending would turn dormancy into a back door to the log of a game still in progress — but nothing states that yet, here or in 05.
- Drop-box turn completion (SVC-448). The collection model needs one rule the platform cannot invent
on its own: in a bundle game there is no server clock and no authority, so what declares a turn's
order set complete when one seat never submits? The candidates are a participant-agreed quorum, an
explicit "close the turn" action available to any participant with the others' standing consent, or a
client-side local timer. This interacts with SVC-414 — under a server it is
CloseOrders, and bundle play has no equivalent — and it must be settled together with 05-multiplayer.md's respecification of its bundle chapter. - Farm capacity and cost (SVC-681, SVC-690). VC-3210 requires ≥ 2,000 games per calibration cell and the cell count is the product of Track × map class × player count; AI-660 requires ≥ 1,000 games per promotion gate and a tuning run may produce many candidates. Nobody has multiplied those out against a measured per-game cost, and until someone does, SVC-690's budget cap is a mechanism with no number in it.