Notifications
Push notifications in the mobile app - the flags, the /push routes, registration and its caps, preferences, and where a tap lands.
Push is a delivery channel of the in-app bell, not a second system: a notification is written once and fanned out to the user's registered devices. The bell is on Notifications.
The two flags
| Flag | Effect when false |
|---|---|
config.notifications.enabled | No bell, no rows, no push - nothing to deliver. |
config.mobile.push.enabled | The bell still works; the app registers no device and the fan-out sends nothing. |
Both must be true: every /push route answers 404 when either is off, and the preference rows hide with them.
The /push routes
| Route | Purpose |
|---|---|
POST /push/devices | Register this device; a repeat is the foreground heartbeat. |
DELETE /push/devices, DELETE /push/devices/:id | Remove this device by token on sign-out, or one device by row id. |
GET / PUT /push/preferences | Read and save the user's per-type choices. |
Every handler reads its user id from the session, so no user reaches another's devices.
| Bound | Value |
|---|---|
| Devices per account | 20. A held token is never refused, so the heartbeat lands at the cap. |
| Registration budget | 30 per minute per user. |
| Unused device retention | 90 days by last_seen_at, swept daily. A dead registration is deleted rather than retried. |
Registration
The Android channel's visible name is its untranslated id default, which is what a reader sees in the OS settings in every language. A translated name needs a NEW channel id and a server change, because Android ignores a rename.
Preferences
Each user chooses which notification types reach their phone, and only the silenceable ones are listed - MUTABLE_NOTIFICATION_TYPES is the set the save route validates against. Money and security types are delivered unconditionally, and the three team rows are hidden in a single-tenant project.
Widen that set for your own product's types only: adding a money or security type lets a user silence a notice you must deliver.
Where a tap lands
A push carries the same deep link as the bell row, and the app's route table is an allowlist over it. The link is normalised - origin dropped, locale prefix stripped - and followed only where the table declares that route or a payload route above it. Anything else opens the notification's own detail screen, and a signed-out tap resolves through sign-in first.
Credentials
| Name | Where | Purpose |
|---|---|---|
| APNs key, FCM V1 credentials | EAS, through eas credentials | Signing the pushes; never in your repo. |
EXPO_ACCESS_TOKEN | The backend's environment | Optional. Sends the fan-out as your Expo account. |