Repository navigation
Native iOS app: push notifications (backend and opt-in) - #140
Conversation
register_push_token writes only the caller's user ID and moves a token when a second account signs in on the same phone. Clients read and delete their own rows; tickets are server-only and keyed on the token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Messages are built from the notification of the user who owns each token row, sent in batches of 100, and DeviceNotRegistered tickets or receipts turn the token off. EXPO_ACCESS_TOKEN is optional. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ai function sends the push after the scorecard is saved, through EdgeRuntime.waitUntil so the response does not wait for Expo. The message names only the round, never the motion, and goes only to the scorer's tokens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Notifications switch asks for permission and files the phone's Expo push token under the signed-in user through register_push_token. The app registers again on launch and when the language changes, and removes the token on sign-out while the session still works. Changes run one at a time, so a sign-out never races a registration in flight. Builds without an EAS project ID cannot get a token, so until app.json has one the switch shows as unavailable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SCREENS=you,timer limits a run to those groups, so a change to one screen does not reshoot the whole app. The You group also shoots the Notifications switch turned on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
supabase/README.md covers register_push_token and the RLS around it, the sender's rules, the one wired trigger, where trial_ending and announcements would hook in, and the deploy order. EXPO_ACCESS_TOKEN is optional and unset. The app guide says no build gets a token until app.json has an EAS project ID, and that prebuild adds the aps-environment entitlement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The run now prints the sign-in screen's alert and shoots the screen with the code field emptied, instead of timing out on the missing tab bar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/07b-push # Conflicts: # apps/mobile/AGENTS.md # apps/mobile/scripts/screenshots.ts
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Note 🎁 Summarized by CodeRabbit Free
Comment |
|
Local checks at 3af8227 (after merging feat), each under the shared lock: npm run verify exit 0 (vitest 311/311, collaboration 30/30, edge OK, build OK, smoke 30/30), mobile typecheck exit 0, npx eslint apps/mobile exit 0, npx knip exit 0 (two existing config hints). The database gate passed before the merge (438 pgTAP, 71 browser); the merge brought no supabase changes. GitHub Actions jobs fail in 2 s with no logs (out of minutes), so these are local results, not CI. |
|
Codex review r1 at 3af8227 (gpt-6.1-sol, xhigh): 3 Medium findings, accepted; fixes in progress.
|
Two register_push_token calls at once each saw only their own new row under READ COMMITTED, so together they could keep eleven tokens. The function now takes a per-account advisory lock until it commits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A read and a change shared one request counter, so foregrounding the app during a change bumped it and the change never cleared busy or showed its failure. Only the newest change now owns busy and failure, the newest read or change still owns the state, and reads queue behind a change in flight so they report where it ended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ands late If a registration was still out when sign-out's five seconds ran out, the queued removal ran later with the already-aborted signal, failed, and then cleared the local record anyway, so the account's token stayed enabled for the next person on the phone. - The removal carries the account's own access token, which keeps working after sign-out until it expires, and has its own deadline instead of sign-out's wait. It deletes every row this install filed for the account. - The local record is cleared only after the server delete succeeds. If it never does, the next account on the phone releases the token through the new release_push_token RPC before doing anything else with push. - A first registration is recorded before the request, so one whose answer was lost is still removed on sign-out (or by the next launch). - Every queued round of push requests has a 20 second deadline, so a hung token fetch or request cannot hold the queue, and registration sends the requesting account's token so it cannot be filed under the next one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Round-1 fixes pushed at a178cf7: c006516 serializes token registration per account (cap respected under concurrency), 4d87f70 keeps the Notifications switch usable when the app is backgrounded mid-change, a178cf7 removes a signed-out account's token even when its registration lands late. Local at a178cf7: npm run verify green, mobile typecheck, eslint, knip pass; pgTAP passed on an alternate port (the shared 54322 port was held by an unrelated process, so the full db gate wrapper couldn't start). Codex r2 (xhigh) running. |
|
Codex review r2 at a178cf7 (gpt-6.1-sol, xhigh): r1's three fixes hold (lock, RPC authorization, token transfer, additive migration). One new Medium, accepted, fix in progress: push.ts:138, sign-out cleanup can clear the local registration record while an earlier register_push_token call is still uncommitted server-side (HTTP cancellation doesn't cancel the statement), so the previous account's token can end up enabled on the next account's phone. |
Cancelling an HTTP request does not cancel its database statement. If account A's registration hit the 20 second deadline, sign-out's delete could run first, see no row and succeed, and the app cleared its local record. When the registration committed afterwards, nothing on the phone knew about it, so A's notifications kept reaching the next account. - Every registration carries the ID of the app's local registration record. release_push_token withdraws that ID and deletes the rows it wrote, the phone's token and the caller's own rows from the install. register_push_token refuses a withdrawn ID. Both hold the ID's advisory lock until they commit, so a release after a registration deletes its row and a registration after a release is refused. - Sign-out, the switch and the next account's cleanup all release through that RPC, and the local record is cleared only after a release succeeds. - A test reproduces the late commit. It fails on the previous code and passes now. pgTAP covers refusal, deletion by registration ID and the lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Round-2 fix 14ead8e (local, pushed with the next merge): the server now refuses a released registration. Each local registration carries a random id; release_push_token records it as withdrawn and deletes its rows in one transaction; register_push_token refuses a withdrawn id; both take an advisory lock on the id, so a late commit is either deleted by the release or refused. New forward-only migration 20261014200000_push_registration_withdrawal.sql. The regression test reproducing the late-commit ordering fails before and passes after; a two-session race on real Postgres left 0 rows both ways. Local: verify green (317 vitest, edge 342), db gate 448 pgTAP PASS, typecheck, eslint, knip. Merging feat/native-ios next (conflicts with #139/#141/#142), then Codex r3. |
Brings in Sparring (#138), development-build prep (#142), App Store entitlements (#141) and offline reading (#139). - The push migration becomes 20261014130000_push_tokens.sql so it sorts after 20261014123154_app_store_entitlements.sql. - auth.tsx and _layout.tsx keep both the push wiring (release on sign-out, refresh on launch and language change) and setCacheOwner/watchNetwork. - package-lock.json is feat's lock plus `npm install` for expo-notifications. - app.config.ts is unchanged: #142 already strips aps-environment from development builds. The mobile guide now says so, and that real push needs a profile with push and an APNs key from Adam's Apple Developer account. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged feat/native-ios as 2cfee5e (Sparring, dev-build prep, entitlements, offline reading). auth.tsx keeps both the offline cache owner and the push release on sign-out; _layout.tsx keeps watchNetwork and refreshPush; lockfile regenerated with npm install and proven with npm ci; push_tokens migration renamed to 20261014130000 so both push migrations sort after the entitlements migration (never deployed anywhere). Local at 2cfee5e: npm run verify green (342 vitest, edge 380), db gate 531 pgTAP PASS, mobile typecheck, eslint, knip. Codex r3 (xhigh) next. |
|
Codex review r3 at 2cfee5e (gpt-6.1-sol, xhigh): withdrawal ordering, definer authorization and search paths, RLS/grants, migration ordering, the merged push/offline-cache hooks, lockfile and the unchanged Expo Go config all checked. One new Medium, accepted, fix in progress: release_push_token deletes by token regardless of registration id, so a delayed release of A's registration can delete B's newer registration of the same token (B's switch stays on but notifications stop). The fix scopes a release to the rows its own registration wrote. |
release_push_token deleted the phone's token whatever registration had written it, and every row the caller's install held. If account A's sign-out release timed out but still reached the database later, it could run after account B had released A's registration and turned notifications on with the same token, and delete B's row. B's switch stayed on while its notifications stopped. The same happened to A itself when it signed back in and registered again under the old ID. - release_push_token(p_installation_id, p_registration) replaces the version that took the token. It deletes only rows whose registration_id is the released one, plus the caller's own rows from the install that carry no registration ID. Withdrawal and the refusal of a late registration commit are unchanged. - The app marks its local record released before it sends a release. A released record is released again before any other push change and reads as off, so the next registration always gets a new ID and a late release cannot reach it. - pgTAP reproduces both orderings (another account and the same account) and fails on the previous function. A push.test.ts case reproduces the same-account ordering and fails on the previous push.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Round-3 fix 2167cad: new forward-only migration 20261014220000_push_release_by_registration.sql replaces release_push_token with (p_installation_id, p_registration): a release deletes only rows its own registration id wrote, plus the caller's own id-less rows from that install, never by token. The app marks a record released before sending the release, re-sends it before any other push change, and always registers with a fresh id. New pgTAP cases for the finding's ordering and the same-account variant fail on the old function and pass now (db gate 533 PASS); push.test.ts same-account case fails before, passes after. Local: npm run verify green (343 vitest, edge 380, smoke 30), typecheck, eslint, knip. Codex r4 (xhigh) next. |
|
Codex review r4 at 2167cad (gpt-6.1-sol, xhigh): no findings. Every ordering checked (late registration commit, delayed release across and within accounts, lost release response, token transfer, two installs, fresh ids), plus the upgrade from 20261014200000 (old 3-argument function dropped with its grants; definer auth, empty search path and registration-scoped locking kept). Merging into feat/native-ios. Nothing deployed; real push delivery needs a build with the push entitlement and an APNs key from Adam's Apple Developer account. |
Push notifications: the backend, and the opt-in in the app. Nothing is deployed. Real delivery to a phone needs an Expo project id (
eas init) and, for iOS, an APNs key.What's in it
push_tokens: the token is the primary key, and only Expo token formats are accepted. A user can read and delete their own rows only, and clients can't insert or update.register_push_tokenRPC. It files the token under the caller and moves it from another account when a second person signs in on the same phone. It also replaces the old token from the same install, caps each account at 10 tokens, and turns a disabled token back on.push_ticketsis service-only and holds no user id.DeviceNotRegisteredtokens.sparring_scored. It fires after the scorecard is saved inai.trial_ending(it needs a scheduled job) and announcements.mobile.you.notifications.*in en, es and hu.Checks (local; GitHub Actions is out of minutes)
./.superset/db-gate.shexit 0: pgTAP 20 files and 438 tests, and the full-stack browser suite 71 passed.npm run verifygreen, includingtest:edge342/342.npm run verifyexit 0: vitest 311/311, collaboration 30/30, edge OK, build OK, smoke 30/30.npx eslint apps/mobile: exit 0.Not provable yet
aps-environmententitlement. The borrowed Signulous profile has no push entitlement, so the dev build has to handle that; it's tracked in PR 9a.Codex review rounds (xhigh, security-sensitive) follow as comments.
🤖 Generated with Claude Code