Built from source via a git submodule pinned to a tag, not a published image — the Dockerfile, main.ts, and manifest should never need to change for an ordinary version bump, only the submodule pin (see UPDATING.md). One volume, one interface serving the browser client + lobby/intent API + SSE stream from a single origin, no dependencies. The server-wide join secret (D14) is seeded on install, exposed via the Get Join Secret action, and blocks start behind a critical task until retrieved — the same first-set/rotation pattern as actual-budget-startos's admin password. Verified on phoenix.local: installs, the critical task correctly blocks an ordinary start, force-starting confirms the daemon binds its port and serves the client, and store.json correctly holds the install-seeded join secret. Not verified: the Get Join Secret action's execution end-to-end — start-cli's `package action run` fails with a client-side deserialization error that reproduces identically against actual-budget's already-shipped equivalent action, so this looks like a start-cli issue rather than a defect here. icon.svg is still the scaffold's hello-world placeholder — no real Station Master icon exists yet to ship in its place.
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.mdfor 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
- Volume and Data Layout
- File Models
- Dependencies
- Network Access and Interfaces
- Installation and First-Run Flow
- Actions
- Tasks
- Health Checks
- Backups and Restore
- Limitations and Differences
- 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/<gameId>/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, holdsjoinSecret(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.tsreads 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 —
checkPortListeningagainst 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
- 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.mdin the game repo), not something this package changed. - 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.
- 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
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