Station Master Logo

# Station Master on StartOS > Everything not listed in this document should behave the same as upstream > Station Master. If a feature, setting, or behavior is not mentioned here, > the upstream documentation is accurate and fully applicable — see the > Documentation section of `instructions.md` for links. Station Master is a railroad operations board game with an authoritative multiplayer server; solitaire needs no server and is not what this package is for. This package runs that server — the browser client, the lobby, and the intent/SSE API — as a single StartOS service. --- ## Table of Contents - [Image and Container Runtime](#image-and-container-runtime) - [Volume and Data Layout](#volume-and-data-layout) - [File Models](#file-models) - [Dependencies](#dependencies) - [Network Access and Interfaces](#network-access-and-interfaces) - [Installation and First-Run Flow](#installation-and-first-run-flow) - [Actions](#actions) - [Tasks](#tasks) - [Health Checks](#health-checks) - [Backups and Restore](#backups-and-restore) - [Limitations and Differences](#limitations-and-differences) - [Quick Reference for AI Consumers](#quick-reference-for-ai-consumers) --- ## Image and Container Runtime Built from source with a custom `Dockerfile` — there is no published Station Master image. | What to Document | Value | | --- | --- | | Image source | Custom multi-stage `Dockerfile`: `node:*-slim` builder runs `npm run build:web` to produce the static client, then a second `node:*-slim` stage copies only `package.json`, `src/`, and the built `dist/` — no `node_modules` in the runtime stage, since the server has zero runtime dependencies | | Architectures | x86_64, aarch64 | | Entrypoint | `node src/server/index.ts` — runs straight from TypeScript source; no compile step, see Limitations | One subcontainer: `station-master-sub`, running the single daemon `server`. ## Volume and Data Layout One volume, `data`, mounted at `/data` (`DATA_DIR`). | What to Document | Value | | --- | --- | | Volume names | `data` | | Mount points | `/data` | | StartOS files | `store.json` — holds the join secret (see File Models) | | Database | None — flat files. Each game is `games//game.json` (`{ engineVersion, seed, config, playerNames, history, status, createdAt }`) plus `turn-timings.json`, an `index.json` naming every game, and lobby state for games not yet started | ## File Models One StartOS-managed file, `store.json`, on the `data` volume. - **`store.json`** — JSON, holds `joinSecret` (a string). Seeded on install with a random 24-character value (`init/generateJoinSecret.ts`) — the daemon will not start without one, so it is never left unset. Rewritten only by the **Get Join Secret** action, which generates a new value on every run; the value is otherwise never touched, so a hand edit to it survives until the action is next run. `main.ts` reads it reactively, so a rewrite restarts the daemon with the new value automatically. Everything else under `/data` — `games/`, `index.json`, lobby and session state — is the application's own persistence, written directly by the server process, not by a StartOS file model. ## Dependencies None. ## Network Access and Interfaces One interface, `ui`, on the single port the daemon listens on. It serves the browser client, the lobby and intent HTTP API, and the per-seat SSE game stream — all from the same origin, which is what lets a player reach the server from a LAN address and another player reach it from a different one in the same game. ## Installation and First-Run Flow No setup wizard. On install, a join secret is generated and stored immediately (see File Models), and a critical task is raised pointing at **Get Join Secret** — the service starts and is usable the moment the daemon is healthy, but a player cannot create or join a game until the join secret has been retrieved and shared with them. ## Actions - **Get Join Secret** (`get-join-secret`) — run this any time you want to read the current join secret, or to invalidate it and issue a new one. Every run **generates a fresh secret, overwrites the stored one, and restarts the daemon** with it — there is no read-only mode. Rotating does not disconnect players already seated in a running game (D14's join secret gates the lobby door, not an in-progress session); it only invalidates the old value for anyone who has not yet joined or created a game. Completes in a few seconds, safe to repeat. ## Tasks - **Get the join secret to share with players** — raised on install, severity `critical`. Points at the **Get Join Secret** action. Clears the moment that action is run for the first time; it does not return afterward (running it again to rotate does not re-raise it). ## Health Checks - **Multiplayer Server** — `checkPortListening` against the daemon's port. "Not ready" means the process has not yet bound its port; on a normal start this clears within a second or two; on a cold container start it briefly reflects the daemon starting up. ## Backups and Restore Strategy: the entire `data` volume is backed up wholesale (`ofVolumes`) — every file copied exactly as it sits on disk, nothing dumped and replayed. That includes every game's full intent history, so a restore can resume any in-progress game exactly where it left off, and the join secret in `store.json`, so previously shared join links keep working after a restore. Nothing needs to be rebuilt or re-entered after a restore; the server resumes every saved game on boot the same way it does after an ordinary restart. ## Limitations and Differences 1. **No compile step.** The image runs the server directly from TypeScript source using Node's native type stripping, rather than building a `dist/` for the server the way the browser client is built. This is upstream's own deployment shape (`docs/architecture/deployment.md` in the game repo), not something this package changed. 2. **The join secret has no per-player identity.** It is a single server-wide value (D14) that gates who may create or join a game — it is not a username or password, and StartOS has no visibility into who a player is once they've joined. 3. **No accounts, and one game at a time per person is expected but not enforced** — a deliberate upstream design decision (`docs/architecture/multiplayer.md` §11 D13), not a StartOS-specific limitation. --- ## Quick Reference for AI Consumers ```yaml package_id: station-master image: built from source (Dockerfile) architectures: [x86_64, aarch64] subcontainers: [station-master-sub] volumes: data: /data file_models: - store.json startos_managed_env_vars: - DATA_DIR - JOIN_SECRET dependencies: none interfaces: ui: { type: ui, port: 8081 } actions: - get-join-secret tasks: - { action: get-join-secret, severity: critical } health_checks: - Multiplayer Server ```