Skip to content

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.

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.

player/inbox.ts holds them, and the server runs them inside changeGarage:

  • hasNews: the envelope’s red dot, for a message newer than readUpTo or a reward not claimed yet.
  • markRead: opening the inbox moves readUpTo to 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.

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.
Terminal window
npm --prefix party run message:send -- --title "Nitrous fix is out" --body "..." --reward 2000 --days 14

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

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.

Terminal window
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.

  • Envelope (ui/topBar/inboxButton.ts): fetches the running messages when the menu opens and adds the red dot when hasNews says 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.