JesseandClaude Opus 5 d905c86e69 0.8.1.0:0 — bundle Station Master v0.8.1.0, and date the games in Manage Game
A table pass over 0.8.0.17. The second-digit bump is deliberate: 0.8.1 had been
reserved for the seatless display table, which is getting more thought, and this
batch earned the number on its own.

GAMES IN PROGRESS RESUME, AND IT WAS MEASURED BEFORE RELEASE rather than argued
from the diff. The bundled release changes what sits in the yards, so the risk
was real. All twelve saves on the test box were pulled and replayed through
`tryResumeSession` — the server's own boot check — against the new build: six
resume, six refuse, and the six refusals are the SAME six, at the same moves,
with the same codes, that 0.8.0.17 already logged. Nothing new was stranded.
That replay is the check worth repeating on any release that touches state,
because it answers before the install rather than after.

THE GAME'S OWN FIX, for the README's benefit: the Freight Agent's "clear the red
Inbound box" returned the car to the Classification Yard still marked loaded,
carrying a load already delivered and already paid for. `pooled()` strips a
load's `origin` stamp and keeps `loaded` on purpose — a train can retire at a
Division Point with freight aboard — so the call site's comment, "a car back in
a yard is back in the common supply, carrying nothing", described something it
never did. Coaches suffered worst: detraining takes `coach && !loaded` out of
the Division Yard, so a cleared coach came back as stock that could never unload
a passenger. 18 loaded coaches against 6 empty over five measured games.

THIS PACKAGE'S OWN CHANGE IS THE MANAGE GAME DROPDOWN. It listed each game's
code, seat count and position and nothing about when the game was last touched,
so four games on a server looked alike — reported 2026-09-22: "I've got four
games out there, but I have no idea which one's which or how old they are."
`lastMoveAt` was already on the wire from the server's `summary()` and simply
unused. Each option now carries both an age and a timestamp, because they answer
different questions: "5h ago" answers the one that was asked, and the date
beside it is what you quote when deciding to end a game. A lobby has had no
move, so it reports when it was created and says so.

`when()` moved out of `gamesInProgress.ts` into `utils.ts` alongside a new
`ago()`, shared by both actions so an administrator sees one date format
wherever a game is named — the second copy would have been the one that drifted.

No route, file model or interface changed. README and instructions.md carry the
release, and the notes in all five locales say games in progress are unaffected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MUizFYCMHRWhbWwXhp7WPR
2026-09-22 21:23:53 -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.

Bundled version: 0.8.1.0. A table pass over 0.8.0.17, and a second-digit bump taken deliberately — 0.8.1 had been reserved for the seatless display table, which is getting more thought. The one that changes state: the Freight Agent's "clear the red Inbound box" returned the car to the Classification Yard still marked loaded, carrying a load already delivered and already paid for. pooled() strips a load's origin stamp and keeps loaded on purpose (a train can retire with freight aboard), so the call site's comment — "a car back in a yard is back in the common supply, carrying nothing" — described something it never did. Coaches suffered worst: detraining needs coach && !loaded out of the Division Yard, so a cleared coach came back as stock that could never unload a passenger. 18 loaded coaches against 6 empty over five measured games.

Games in progress resume, and it was measured before release, not argued from the diff: all twelve saves on the test box were replayed through tryResumeSession — the server's own boot check — against the new build. Six resume, six refuse, and the six refusals are the same six, at the same moves, with the same codes, that 0.8.0.17 already logged. That replay is the check to repeat on any release that touches state, because it answers before the install rather than after.

Also: leaving a running game no longer strands you in a lobby with no way to join or create (runLobby never restored the section enterSeating hides); the lobby's action buttons carry the board's amber; an Enhancement in the history names the Mainline card it went on, slot and type, by its printed name; the Manage Game dropdown says how long ago each game was last touched; and §6.2's Department refill — always implemented, never documented — is written into the game's rules reference.

Previously, 0.8.0.17. Four fixes from a live playtest of 0.8.0.16, every one of them a case of the game knowing something and the screen not saying it. ABS Signals could be played on only one Mainline card although its own tooltip said "any": the engine allowed all of them, but the action list drops duplicate labels and every placement described itself identically, so all but the lowest-index card were discarded before the menu saw them. A card carrying ABS now draws a signal mast, and the three Heavy Grade modifiers draw as BRK, AIR and HLP — none of them marked the board before, so the only way to learn one was there was to hover it. A Freight Agent turn claimed a car moved when none had (§6.3 requires no action, and the bot declines on purpose rather than wreck a stocked box); it now says what it may do, says so when it idles and why, and names the industry rather than a grid coordinate. And the log and the action list spelled the same square differently — one in map order, one in storage order — which is fixed in favour of the map.

The full documentation is now served by the package and reachable from inside a running game, under the This Game card: Quickstart, Rules, every card, both deck references and Components. 0.8.0.16 published the Quickstart alone and its "Where to read more" links all 404'd. Games in progress resume normally — no rule changed.

Previously, 0.8.0.16. Six fixes from a live playtest of 0.8.0.13, plus two follow-ups. One is a rules change and the rest are the game saying what it was already doing. The Sparrow running empty and a Depot that could not unload its passengers turned out to be the same thing: §9.2 sends an emptied coach to the Classification Yard and takes a fresh empty out of the Division Yard, and §2.2 returns Classification only when the Division Yard runs bare — so coaches travel one way. Measured over one three-Day game: sixteen coaches in the Division Yard at setup, none from Day 2 Stage 8 onward, fifteen piled in Classification while the Division Yard sat at 46-47 freight cars and stopped draining. The rule stands (Jesse's ruling, the same one Gitea#2 got); what changed is that a train made up short now reports what it wanted and why none is coming, and the yard panel warns while the shortage lasts.

Also: a Small Yard sort shows the train each option would build, laid out west to east as the board draws it and saying whether the result may leave the Office, and may now leave cars ahead of the engine; making up a train names the train in the history; a switching turn keeps its first move, industry work, the sort and a closing summary instead of a line per move; and the Freight Agent, Porter and Laborer groups say what each role is for.

Two smaller releases ride along. v0.8.0.15 stopped the Interchange advertising a car-sorting action it has never had — the text reached players on the board through mainlineDescription, which view.ts renders as a Mainline card's description, so a player could only go hunting for a button that was not there. What the card actually offers is the one place on the Mainline an Extra may be made up and started. That release also carries a Quickstart for new players and brings the four hand-written reference documents up to the game as it runs. As of 0.8.0.17 all of them are served by this package — /quickstart.md, /rules.md, /home-deck.md, /mainline-deck.md, /components.md and the generated /rules/as-built.md — linked from the splash page and from the This Game card in a running game. They are served as text/plain, so their tables render as rows of pipes; rendering them as styled pages is filed upstream as TODO #109. v0.8.0.16 is a doc comment on the flag behind that card, which is the most likely source of the wrong sentence in the first place.

GAMES IN PROGRESS MAY NOT RESUME ON THIS ONE, and it was measured rather than assumed. A Modifier card may no longer be placed outside a player's Limits, which reverses an earlier ruling and is the one shape of change that strands a save: a predicate in apply.ts changed its answer, so a move that was legal when played is refused on replay. The Day 3 playtest save stops at intent 528 of 539 — card.play c55 → (-2,4) against a sign at column 3 — eleven moves short of where the table had got to. The file is left untouched and the server names the move it stopped at, so re-installing 0.8.0.13 finishes a game worth finishing. (0.8.0.10 was the first release to do this, for the §8.1 clearance correction.)

The rest of the engine diff strands nothing: makeUpShort and switchingEnded are new events, trayMoved and consistSorted gained fields, and switch.sortConsist gained an OPTIONAL engineAt whose absence means what every sort did before — so an older save replays to the same train. Events are derived by replaying a save rather than stored in one.

The paragraphs below were written for the 0.7.9.8 bump and still describe how a bump is checked.

Why saves are safe this time, and how that was established. 0.7.9's breakage came from a rules change, and the general rule behind it is that a save is a seed plus the moves played, replayed through the current rules — so any change that makes a once-legal move illegal stops an older save. The question for this bump is therefore narrow: did anything become illegal? git diff v0.7.9..HEAD -- src/engine/ is, in its entirety, four things — isExpedited exported and widened to a structural parameter type, isFreight exported, one new read-only helper (freightRuleSpentHere, which only the blocked panel asks), and check('draw.end')'s inline hand-limit test replaced by a call to overHandLimit(state, player) holding the identical expression. No predicate changed its answer. Eight releases, none of them in the rules.

Note that engineVersion is not a gate. src/server/index.ts attempts the replay and reports the stored and running versions only in the failure message; a game is refused because a move no longer replays, never because the version string differs.

Two multiplayer information leaks. The random seed was announced in the shared narration log at game creation, and a blind Home Office draw resolved the drawn card to its real name for every seat. Both were found by reading a plan rather than by a test, which is why 0.7.9.4 added a systematic redaction net: a seat's Frame, the spectator projection and the narration are serialised and searched for every opponent card id, every card name unique to one hand, the seed, and any private decision data — with an allow-list of every public property that fails the suite when a field is added.

The history panel came back empty after a mid-game reload. Frame.lines carried the whole narration log on every push to every seat and nothing read it — RemoteSession accumulates from Push.lines alone. The waste was masking the fault: connect() cleared the frame cache but not the narration watermark, so a reconnecting seat was told "nothing new since your last push" while the browser it was answering had just reloaded from an empty accumulator.

Whose turn it is, during the extended-play vote. The §3.3 vote is parallel — every un-voted seat may vote at once — so there is no actor to be, and the engine says so. The turn chart named the last seat to move anyway, beside a tally correctly showing three seats outstanding. Cause: two functions answering one question, one carrying a status guard and one not; there is one now, and it is the same function that refuses an intent, so the screen can no longer name somebody the server would turn away.

A train held at the Limits was drawn nowhere. An Interlocking stops an inbound train on the Limit Track instead of colliding with a full Office. arriveAtOffice removes the tray from the Mainline node's transits and the Interlocking branch never assigns tray.position, so with the map drawing mainline nodes from transits and squares from position.at === 'grid', the train was in neither — it vanished off the board until an A/D track freed. Fixed in the view; the engine state was right.

Also in this range. Trains queued for a Crew Tray (a Timetabled departure, a played Extra, an ordered second section) now report themselves with the free-tray count, where two of the three had been waiting invisibly behind an exhausted pool. A Red Flag standing at an Office's Limits is drawn on the map. A spent Telegraph/Telephone/Radio is struck through and says when another district holds the Fedora — it is the Superintendent's own devices that get spent, and the Fedora moves every three Stages. The Campaign Train says whether its speeches are made, which is what decides whether leaving it off the Office square is a fault. Two bugs from the last playtest session: the Express's one freight car per location rule is explained where it refuses you (the panel had been describing the industry instead), and westbound trains are drawn in the correct half of a Mainline card.

Bundled version: 0.7.9. Enforces solitaire's collision limits, which had been offered as settings and never checked — and that is a rules change, so a solitaire game in progress will not resume if it has had collisions.

advance.ts gated §3.4's check on mode === 'competitive' || mode === 'coop', while SOLO_CONFIG carried both limits (3 a Day, 5 total) and the setup screen offered them as live settings with "the game ends in a loss" printed beside them. A solitaire player could set a limit of 1 and crash all game. Jesse's ruling: the settings do what they say, so the gate went rather than the controls.

Why saves break, and how it was measured. A solitaire save is { seed, history, rules? } where rules is the house rules only — the collision caps are not in the save, so a restore takes today's defaults. A 0.7.8 save therefore replays under live caps and stops at the move that crossed one. Measured with a control, because two earlier attempts were wrong: with the caps disabled 1200 of 1200 generated saves replay every intent; of those, 74 crossed the 5-collision total and 70 of the 74 truncate once the caps are enforced, one at 141 of its 532 intents. An earlier run that reported no breakage had in fact been failing every replay at intent 2 on an unrelated house-rules mismatch — the control is what caught that. It fails safe: the save is untouched and the game declines rather than loading a position the rules could not have produced. Multiplayer games are unaffected — competitive and coop were always inside that gate.

Also in 0.7.9. The end-of-game dialog now asks whether to play one more Day; the buttons existed but were written into #actions underneath a modal whose only control was Close, so a solitaire player reaching the end was never offered the extension — and the Gitea#11 verification missed it because it drove the HTTP API, which renders no dialog. Frame.actor carried clock.currentActor, null for the whole Mainline Phase, so all three interruptions reported that nobody was holding the game up; it carries actingPlayer and an awaiting field now. The game settings moved off the top line into a This Game card drawn by the same renderer as the lobby's join preview, with the running collision counts taking their place on the top line. The Office Area's auto-hide button was a cycle that could not reach every state and is now three controls. The history reads newest first. A Heavy Grade card draws which way it climbs.

Bundled version: 0.7.8. Made the solitaire setup screen reachable at all. 0.7.5 skipped it whenever load() found a saved game — reasoned as "a saved game is a game to resume" — and a browser that has ever played solitaire always has one, so the door could never reach the screen again. The door (?solitaire) outranks a save now; a bare reload still resumes. Since dealing calls clearSave(), the screen carries a Continue saved game button and states what Deal costs, so the door cannot destroy a game in progress. A solitaire save is browser-side, so nothing on this server is involved either way. The splash footer also now names both ways to play.

This was reported three times before it was found, and the first two fixes were real bugs that were not it — a routing fault (0.7.6) and a caching fault (0.7.7). Both were reported as verified, and both verifications read what the SERVER returned rather than exercising the path with the state a returning player actually has. What found it was a failing test written before the fix. Worth knowing when the next "that didn't work" arrives: reproduce the reporter's state first.

0.7.7 fixed the client caching that stopped the two releases before it from ever reaching a browser. build-web.ts stamps a build tag onto every module URL as a cache key, and its fallback when git rev-parse fails was the literal nogit — which is precisely the .s9pk case, since the Dockerfile copies the working tree in without .git. So every packaged release published ./web/main.js?v=nogit, byte-identical to the one before, and a returning browser refetched nothing. serveStatic also sent no Cache-Control at all, so the pages that carry those tags were themselves served from cache. The tag is now the package version plus the build timestamp, and a request carrying ?v= is immutable for a year while everything else is no-cache.

Diagnosing "my fix did not ship". A hard reload is NOT a sufficient check — confirmed in the field on 0.7.6: the document refetches but ES module sub-imports keep their cached ?v= URLs, so the module graph stays stale. A fresh private window is the reliable test. Reading what the server returns (curl inside the container) proves what was installed, never what a browser is running.

0.7.6 fixed the solitaire door 0.7.5 introduced: a browser that had ever held a multiplayer seat could not reach the new setup screen at all — a bare page load could not tell "clicked Play solitaire" apart from "reloaded mid multiplayer game", so the door lost to whatever game or lobby that browser last touched. The door marks its intent explicitly now (?solitaire), the same fix ?lobby already carries for the door on the other side.

0.7.5 was a client-side flow change: a genuinely fresh visit to the solitaire page opens a setup screen and asks for the game's options before dealing, the same question the multiplayer lobby has asked before a game starts since 0.6.0.

0.7.4 bundled three rules corrections off the tracker (Gitea#13, #5, #19), all of them places where the code and the cards disagreed.

The Yard Office is offered rather than imposed (Gitea#5). It was implemented in a form missing all three of its conditions: a qualifying train was teleported onto the card, so nobody was asked, no route was walked — the card's printed "that can reach the yard office in one move" was unenforced — and nothing was ever met on the way in. The arrival now interrupts the Mainline Phase to ask the district's owner, reachability is the engine's own move walk, and cars on the lead collide. Where no route exists the offer is withheld and the history says why.

Some Extras must run loaded (Gitea#13). Circus, Campaign and Military trains take a loaded car while the Division Yard can supply one, an empty once it cannot, and may depart short. The per-stop point is earned once per Office Area rather than once per game, and only by a fully loaded train. Two long-standing bugs went with it: emptiesOnly was rendered to the player and enforced nowhere, and a set-up out on the Mainline paid its point to player 0 whoever was playing.

Red Flags is a different card (Gitea#19). The old rule protected a stopped train on the Mainline and was played 4 times in 4,212 offers across 600 games. It is now a directional flag on your own Limits, spent on the train it stops, playable either in phase or at the moment the engine sees a certain collision.

GAMES IN PROGRESS MAY NOT SURVIVE AN UPDATE FROM BEFORE 0.7.4. Unlike 0.7.3, which changed no rules, each of the three above can stop an older history replaying: the Red Flags intent changed shape, a make-up that was legal may now be refused, and a Yard Office arrival asks a question no older history has an answer for. It fails safe — src/server/index.ts refuses to resume a save the rules reject, logs which move it stopped at, and leaves the file untouched, so an operator can put 0.7.3 back on to finish a game that matters. A game that never meets one of the three carries on normally, which is why this is "may not" rather than 0.7.2's "will not". 0.7.5 carries every game forward without exception — see above.

Diagnosing the interruptions. The Mainline Phase can now stop and ask three different questions, of three different players: §8.1's clearance goes to the Superintendent, the Yard Office offer and the Red Flag prompt to the owner of the district a train is arriving at. A game sitting on one is waiting on a person, not stuck. Games in Progress shows it as active with nobody to wait for, because an interruption is not a turn — the same reading as an extension vote.

0.7.0 remains the shape of the service: the lobby sets a game up from one of four game types (Solitaire, Co-op, Competitive, Cutthroat) plus Custom, offers the whole rule set for reading before a seat is taken, and lets a player leave a lobby or a running game and come back to it. None of this changes anything the package itself does: the submodule pin is the version, and the service is the same server it always was.


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 and admin secrets (see File Models)
Database None — flat files. The server holds any number of games at once: each is games/<gameId>/game.json ({ engineVersion, seed, config, playerNames, history, status, createdAt, lastMoveAt, botSeats }) plus turn-timings.json, with a top-level index.json naming every game and lobby state for those not yet started

File Models

One StartOS-managed file, store.json, on the data volume.

  • store.json — JSON, holding two independent secrets, both seeded on install by init/generateSecrets.ts and both read reactively by main.ts, so rewriting either restarts the daemon with the new value.

    • joinSecret — 24 characters. What players need to create or join a game. The daemon will not start without one, so it is never left unset. Rewritten only by the Get Join Secret action, which mints a new value on every run.
    • adminSecret — 32 characters. Gates the server's /api/games routes, which the two game actions use. Deliberately not the join secret: every player holds that one, so gating a delete with it would let anyone at the table destroy anyone else's game. It is never shown to a player and never leaves the package except as the daemon's ADMIN_SECRET. Backfilled on update for a volume written before this field existed, and otherwise never rewritten.

    Neither key is re-asserted on start, so a hand edit to either survives until the relevant action is next run.

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, both secrets are 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.

Updating the package keeps games in progress, unless the rules actually changed. On boot the server replays each saved game's moves through the current engine and resumes it if they all still apply — the version that wrote the file is recorded and reported but decides nothing. When a move is rejected, that game is refused and the log names it: move 3 of 8 (localOps.choose) is rejected by the current rules with OPTION_ALREADY_CHOSEN. A refused game is never modified or deleted, so reinstalling the previous version makes it loadable again and it can be played out.

Earlier versions of this package compared version strings instead, which destroyed every game in progress on every update, including updates that changed only how the board is drawn.

Actions

Three of the four read or change the games on the server, and all three are only-running: what they report exists only inside the live server process. A save is a seed plus a list of moves, so "whose turn is it" is answerable only by replaying the game through the engine — which lives in the game repo, not in this package. The server has already done that work and is asked for the answer.

  • 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.

  • Games in Progress (games-in-progress) — read-only, changes nothing, safe to run at any time. Lists every game and lobby on the server with its code, players, Day/Stage/phase, who it is waiting on, when it started and when it last moved. Run it to find a game that has stalled — a Last move days old with a named player under Waiting on is someone who is not coming back. Returns quickly; the server answers from memory.

  • Manage Game (manage-game) — pick a game from a dropdown built live from the server, then either Export it (returns the complete save as copyable text and changes nothing) or End it (deletes it from the server, disconnects anyone still watching, and removes its files and index entry). Ending cannot be undone and is not idempotent — a second attempt reports that the game no longer exists. It always returns the deleted game's save, so nothing is destroyed without being handed back first; that text is the only remaining copy, so keep it if the game is worth replaying. This is the only way a game ends other than being played to a finish: an abandoned game otherwise stays active and is resumed on every restart indefinitely.

  • Restore a Seat (restore-seat) — pick a seat from a dropdown of the players actually holding a session, and get back a one-time link that puts that player into it. This is the answer to a lost seat token (Gitea#33 in the game repo): the token is the only identity the game has and it lives in one browser's localStorage, so a cleared profile, a private window or a different browser leaves a player locked out of a game that is still running with their session still on disk. The link carries a code, never the token — single-use, expires in 30 minutes — which the page trades for the real token over a POST as it loads (lobby-and-sessions.md §1: keep the token out of URLs). Anyone who opens the link takes that seat, so it is sent to one person and not to a public channel. Minting is administrative because deciding that somebody has lost a seat is a judgement; spending needs no secret, because the player following the link holds none. The dropdown lists only seats a human holds a token for — bots never appear, read from the server's session map rather than guessed from player names.

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 — fetches the server's own /api/health and reports what it says: "Multiplayer server is ready — 3 games in progress", with ", 1 waiting to start" appended only when a lobby exists, and "no games in progress" on an idle server. So the check that proves the server is answering also says how much is going on.

    A server that does not answer is reported as starting, never failed. The server replays its saved games before binding its port, so a boot legitimately looks like nothing is listening, and calling that a failure would make an ordinary restart look like a crash. Replaying measures around 100 ms per game and only unfinished games are replayed, so in practice this window is a fraction of a second. A check stuck on "starting" for much longer means the daemon is failing to come up — read the service logs, where a refused resume names the game and the version that wrote it.

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. Since 0.7.0 a browser can hold seats in several games at once and picks between them in the lobby, which is a client convenience rather than a change to that decision.
  4. A player's identity lives in their browser. The session token issued at join is the only proof of who a player is; it is kept in that browser's localStorage and nowhere else. A player may leave a running game and rejoin it (the lobby lists every game the browser is in), but a token lost with the browser — cleared site data, a different device — cannot be recovered from this package, and the seat stays in the game waiting. Upstream TODO.md tracks an administrator-issued rejoin link; it does not exist yet.

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
  - ADMIN_SECRET
dependencies: none
interfaces:
  ui: { type: ui, port: 8081 }
actions:
  - get-join-secret
  - games-in-progress
  - manage-game
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%