Jesse 278bbcf51c Real icon, and open straight into the game instead of the splash
icon.png replaces the scaffold placeholder — resized to 512x512 and
palette-quantized (1.9MB source -> 52.9KB) to stay close to the packaging
guide's 40 KiB guidance with no visible quality loss at icon size. Verified
station-master_x86_64.s9pk builds clean with a single icon.* file (the
packer errors on more than one) and `s9pk inspect` confirms icon.png is
packed correctly.

The `ui` interface now opens at `/play.html` instead of `/`. Root serves
index.html, the game's marketing splash — identical to the public static
solitaire site, and with no lobby or Multiplayer button on it; both live on
play.html (src/web/main.ts's `start()`, reached from index.html's door
link). That page is right for a stranger landing on the public site, wrong
for someone who just installed a dedicated multiplayer server and found
what looked like the same solitaire page with no way to host or join a
game. Verified: fetching /play.html from inside the container and over the
service's public address both return the page with the Multiplayer button
present.

Reinstalled on phoenix.local after each change.
2026-08-21 09:09:04 -04:00

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

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, 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

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
S
Description
Multiplayer Service for StationMaster Game
Readme
861 KiB
Languages
TypeScript 98.5%
Dockerfile 1.3%
Makefile 0.2%