Inbox
The envelope in the main menu top bar opens the inbox: messages the team sends to every player, such as an update going out or a bug being fixed. A message can carry a cash reward, which each player can claim once while the message runs. New players who open the game before a message ends can claim it too. The design is in docs/2026-11-08-achievements-inbox-design.md.
Where the data lives
Section titled “Where the data lives”Messages sit in the message table on the race server’s D1 (party/migrations/0009_message.sql): title, body, reward_cash, starts_at and expires_at in ms since the epoch, and version (0016_message_version.sql), set only on a what’s new note. The table holds no player data, so the account delete and export (party/src/db/userData.ts) leave it alone.
What a player read and claimed lives in their save as inbox: { readUpTo, claimed }. claimed only keeps ids of messages still running, so it stays short. The game’s sync never sends it: only the server writes it.
The rules
Section titled “The rules”player/inbox.ts holds them, and the server runs them inside changeGarage:
hasNews: the envelope’s red dot, for a message newer thanreadUpToor a reward not claimed yet.markRead: opening the inbox movesreadUpToto the newest running message.claimReward: refuses a message that ended, has no reward or was claimed already; otherwise adds the cash.
tests/inbox.test.ts covers them; npm --prefix party run test:inbox checks the endpoints against a running wrangler dev.
Server endpoints
Section titled “Server endpoints”party/src/api/inboxApi.ts, signed in. Each call carries ?version= with the game’s version, which picks its what’s new note:
| Endpoint | Does |
|---|---|
GET /api/inbox |
The note for the game’s version, then the running messages, newest first. |
POST /api/inbox/read |
Marks them read; answers with the garage. |
POST /api/inbox/claim { id } |
Pays the reward; answers with the garage, or 409 and why not. |
Sending a message
Section titled “Sending a message”npm --prefix party run message:send -- --title "Nitrous fix is out" --body "..." --reward 2000 --days 14It inserts into the remote D1 (--local for the database npm run dev uses). --reward is whole dollars, 0 for news only. Write the title and body through rsp:write-content first: they go to every player as they are.
What’s new after an update
Section titled “What’s new after an update”A message sent with --version is that release’s notes. Only the game at that exact version gets it, first in the inbox, and it ignores --days: it stays until the player’s next update, which brings the next note. A player who skips a release sees only the newest one.
npm --prefix party run message:send -- --version 0.2.0 --title "What's new in 0.2.0" --body "..."The version is package.json "version": the Vite configs put it in the build (vite/appVersion.mjs, read through core/appVersion.ts) and Android’s versionName reads the same file. iOS still has MARKETING_VERSION in the Xcode project, so bump it by hand. Bump package.json for each store release and send the note when the release goes out; players who update before it is sent see it at their next menu visit. To fix a typo, update the row (wrangler d1 execute DB --remote --command "UPDATE message SET body = '...' WHERE version = '0.2.0'"): a new note with a new id would light the dot again.
A fresh install has nothing new. player/whatsNew.ts keeps the first version a device ran in localStorage, and that version’s note shows in the inbox without lighting the dot. The next update’s note does light it, until the inbox is opened.
In the game
Section titled “In the game”- Envelope (
ui/topBar/inboxButton.ts): fetches the running messages when the menu opens and adds the red dot whenhasNewssays so, leaving out the note of the version this device first ran. Offline it stays a plain envelope. - Inbox screen (
inbox/scenes/Inbox.ts, its own feature module): the messages as cards (inbox/view/messageCard.ts), a Claim button on each unclaimed reward. Messages that do not fit the panel are left out; they run out anyway.