Signing keys
The Ed25519 keyring that signs viewer tokens. Super-user only: one key signs for every casino on the instance, so it is not a resource of any of them.
Why this one has a window
A viewer token lives 15 minutes and is already in the browser of everyone currently watching. Replacing the key outright invalidates all of them at once, disconnecting the entire audience of every running battle.
Rotation therefore appoints a new key and leaves the previous one verifying until it retires:
The kid in each token's header is what lets one verifier hold both keys.
A retirement window shorter than a token's own lifetime is refused, because it would strand tokens that are still valid. Pick a window with real margin rather than the minimum.
Exactly one key is active
Enforced by a partial unique index in the database, not by service logic that two concurrent rotations could race.
Status is derived from the retirement timestamp and never stored, so status and retirement cannot disagree.
Rotation is a POST, not a PUT
It is not idempotent: calling it twice mints two keys and retires two.
Rotation takes effect immediately
Both consumers resolve the keyring per call rather than capturing it at boot. Without that, a rotation would only take effect on the next deploy.
Fail-closed
A keyring with no key errors. It does not fall back to unsigned tokens or skip verification.
Both call sites degrade identically on an error and on a nil: the WebSocket handshake answers
401, and minting answers "not ok". A third consumer would need that equivalence re-checked
before it could be trusted.
The environment variable is bootstrap only
SLOTBATTLE_WS_SIGNING_PRIVATE_KEY_PEM applies only while the key table is empty.
Once a key exists, rotation is the only way to change it. Editing the variable and redeploying has no effect.
Symptoms of no key at all
POST /battles/{id}/viewer-tokensanswers500 failed to mint viewer tokenws_tokenis absent from create and seat responses. It is an optional field, so there is no error to catch- Battles open and fill normally; only live viewing is unavailable
If a casino reports that live view does not work while everything else is fine, check this first.
Rotating safely
- Confirm no unusually long battle is mid-flight.
- Rotate with a retirement window comfortably longer than a token's lifetime.
- Confirm the new key is
activeand the previous oneretiring. - Let the window pass. Existing viewers are unaffected throughout.
Announce it to integrators if you have any. Handled correctly the rotation is invisible; handled without a window it drops their audience, and they will need to know which it was.