Summary
When someone who already uses Scout opens an updated app, a full-screen What's new sheet appears once, at the first calm moment. It sits over the ember backdrop from the unlock celebration and lists what changed, grouped under New, Improved and Fixed. Items can carry a Take me there link straight to the feature. A person who skipped versions sees every release they missed, newest first. It is never shown to someone installing Scout for the first time. The drawer's What's new row shows the recent releases at any time.
This replaces the release-notes post that used to be pinned to the community board. Those old posts stay where they are, but no new ones are made.
Status
Built, not yet released (2026-09-27). It ships in the app release after 1.1.95
(1.1.95 was already in review when it was built). The backend endpoint ships with
the next backend deploy, and the new table is created by that deploy's prisma migrate deploy. No feature flag. Update this line when the build is live.
User-facing surfaces
- Launch sheet:
mobile/app/whats-new-modal.tsx, afullScreenModal(mobile/app/_layout.tsx:459). Layout "A + Embers + Got it" ofmobile/research/whats-new-lab.html: an X top left, "What's new" and the release date (plus "N updates since you last looked" when versions were skipped), then the notes, then a gold Got it button pinned at the bottom. Background:EmbersBackground, the same one as the unlock celebration and the onboarding reveal. - Drawer → What's new:
mobile/app/(drawer)/whats-new.tsx, deep linkscout://whats-new, in the MORE section before Settings (drawerSections.ts:374), visible to everyone. The latest 3 releases, newest first, under the standard drawer header, over the same embers. No X, no Got it. Loading skeleton, error with Try again, empty state. - Deep link to the sheet:
scout://whats-new-modal. Opened with nothing new, it shows the latest releases instead of an empty sheet.
How it works
-
Publishing. The deploy-apps skill drafts structured notes and publishes them once the release is installable with
npm run release:notes -- --file <json>(backend/scripts/release-notes-publish.ts). The script validates locally, then POSTs toPOST /api/admin/release-notesthroughProdAdmin. Every write goes throughvalidateReleaseNotes(backend/src/release-notes/release-notes.validate.ts):- the version looks like
1.2.3 - sections are titled New / Improved / Fixed
- each item has 1–3 bullets and a known icon
- links must be
scout://deep links, never the web - republishing a version replaces it, which is how a typo gets fixed
- the version looks like
-
Reading.
GET /api/release-notes?upTo=<installed>[&after=<last seen>]is public (release-notes.controller.ts:10).selectReleaseNotes(release-notes.service.ts:32) compares versions numerically (1.1.10 > 1.1.9). Withafterit returns every version in (after, upTo]; without it, the latest 3 ≤ upTo. It never returns notes newer than the installed app. -
Remembering what was seen. The app persists
lastSeenReleaseVersionon the device, next totutorialSeen.- Finishing onboarding records the installed version (
store.ts,setTutorialSeen), which is what keeps a brand-new user from ever getting the sheet. - Presenting the sheet records it (
presentWhatsNew). It is recorded when the sheet opens rather than when it closes, so no way out (X, Got it, a link, the Android back button) can bring it back next launch. The same call freezes the notes the sheet shows (presentedReleaseNotes, not persisted), so the content cannot change while the sheet animates away.
- Finishing onboarding records the installed version (
-
Deciding at launch (
mobile/src/utils/whatsNewGate.ts):whatsNewQuery(:34):- nothing while onboarding is unfinished, or when this version was already seen (or the app was downgraded)
- "everything since last seen" for an update
- "latest only" for a device with no record yet, which is an existing user on their first update after this feature shipped
pickLaunchNotes(:42) then keeps, for that last case, only the installed version's notes, never the back catalogue. A version with no notes (a hotfix) shows nothing.
-
Waiting its turn.
WhatsNewWatcher(mounted besideCelebrationWatcher,_layout.tsx:452) presents the sheet only whencanPresentWhatsNew(:56) agrees:- the user is on an ordinary drawer screen (Home and its siblings), not onboarding, a celebration, or a pushed screen
- no unlock celebration is owed
- there are notes
- it has not already shown this session
The calm moment must also last
WHATS_NEW_SETTLE_MS(1.5 s) and still hold when it is re-checked. CelebrationWatcher dequeues the last owed patch a moment before the celebration route opens, and without the settle the sheet would slip in on top. Celebrations are blocked over the sheet in turn (celebrationGate.ts:33), so the two never stack. -
Take me there dismisses the sheet and then navigates (
router.dismiss()thenrouter.navigate(path)), the same as the celebration's "view patch". Arouter.replacefrom this root modal would stack a second drawer navigator over Home for a drawer route. -
Dates are formatted in UTC (
formatReleaseDate), so a release published for Sep 27 never reads "Sep 26" in the US. -
Malformed data never crashes the sheet.
sanitizeReleaseNotesdrops any item or section the app cannot render. The server validates every write; this is the second line of defence against a hand-edited row.
Data model
release_notes (migration 20260927120000_release_notes), one row per version:
version (PK text), released_at, sections JSONB, created_at, updated_at.
sections = [{ title: New|Improved|Fixed, items?: [{ icon, heading, bullets[1-3], link?: { label, url: scout://… } }], fixes?: string[] }]. On the device:
lastSeenReleaseVersion in the persisted store.
API surface
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/release-notes?upTo=1.1.96&after=1.1.93 |
none | Releases in (after, upTo], newest first |
| GET | /api/release-notes?upTo=1.1.96 |
none | Latest 3 releases ≤ upTo |
| POST | /api/admin/release-notes |
AdminGuard |
Validate and upsert one release's notes |
Key files
backend/src/release-notes/:release-notes.validate.ts(the shape and the gate),release-notes.service.ts,release-notes.controller.ts,release-notes.module.tsbackend/scripts/release-notes-publish.ts:npm run release:notesmobile/src/utils/whatsNewGate.ts: every launch decision, puremobile/src/hooks/useWhatsNew.ts,mobile/src/api/releaseNotes.ts,mobile/src/query/queries/releaseNotes.tsmobile/src/components/whats-new/:WhatsNewContent.tsx(shared body),WhatsNewWatcher.tsx,whatsNewIcons.ts(must matchRELEASE_NOTE_ICONS)mobile/app/whats-new-modal.tsx,mobile/app/(drawer)/whats-new.tsx- Design:
mobile/research/whats-new-lab.html(variant "A + Embers + Got it", chosen 2026-09-27)
Configuration and flags
No feature flag. DRAWER_LIMIT = 3 (release-notes.service.ts:7). The notes are
cached for 5 minutes on the device.
Edge cases and known limits
- Offline or a failed fetch at launch shows nothing, and it tries again next launch. Only the drawer page shows an error.
- Seen means shown. The version is recorded when the sheet opens. A user who kills the app the instant it appears does not get it again.
- Notes published late still arrive. A launch that finds no notes for the new version presents nothing and records nothing, so a later launch (once the notes are published and the 5-minute cache has expired) shows them.
- Once per version, per device. The record lives on the device, so a reinstall starts fresh, and a reinstalled returning user is treated as new (onboarding again).
- A hotfix release with no notes shows nothing, even to a user who updated to it.
- Existing users see only the current release on their first update after this ships, because they have no record yet.
- The icon lists must match. The backend's
RELEASE_NOTE_ICONSand the app'sWHATS_NEW_ICONSare two copies in two projects. An unknown name falls back to a sparkle on the device, but the validator refuses it first.
What this feature does NOT do
- It never shows to a brand-new install. Finishing onboarding marks the installed version as seen.
- It is not a push notification. Nothing is sent. It appears only when the user opens the updated app.
- It does not post to the community board any more. Old RELEASE NOTES posts remain
but are no longer created. (
release:announcestill exists inbackend/scriptsbut the deploy skill no longer runs it.) - It does not show notes for versions newer than the installed app, even if they are published.
- It does not interrupt anything. It never appears over onboarding, a celebration or a pushed screen, and never while a celebration is owed.
- There are no images, promos or per-user content. Just headings, bullets and in-app links.
- Reading it from the drawer does not mark it seen. Only the launch sheet opening does.
Tests that cover it
backend/src/release-notes/release-notes.validate.spec.ts: every validation rule, and numeric version comparison.backend/src/release-notes/release-notes.service.spec.ts: version range, newest first, never newer than installed, drawer limit, malformed versions.backend/src/release-notes/release-notes.controller.spec.ts: query passing, 400 on bad notes with nothing written.backend/src/release-notes/release-notes.db.spec.ts(real Postgres): migration from scratch, JSONB round-trip, republish replaces in place.mobile/src/utils/__tests__/whatsNewGate.test.ts: new user, update, skipped versions, first-ever, downgrade, hotfix, the calm-moment rule.mobile/src/domain/__tests__/store.lastSeenReleaseVersion.test.ts: onboarding records the version, persistence, legacy devices.mobile/screen-tests/whats-new-modal.test.tsx,mobile/screen-tests/whats-new.test.tsx,mobile/hook-tests/whatsNewWatcher.test.tsx(paired present / hold-back cases).
Open questions
- Whether to backfill notes for releases before this feature. Only versions
published with
release:notesexist.