GenerateSaaS

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

FlagEffect when false
config.notifications.enabledNo bell, no rows, no push - nothing to deliver.
config.mobile.push.enabledThe 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

RoutePurpose
POST /push/devicesRegister this device; a repeat is the foreground heartbeat.
DELETE /push/devices, DELETE /push/devices/:idRemove this device by token on sign-out, or one device by row id.
GET / PUT /push/preferencesRead and save the user's per-type choices.

Every handler reads its user id from the session, so no user reaches another's devices.

BoundValue
Devices per account20. A held token is never refused, so the heartbeat lands at the cap.
Registration budget30 per minute per user.
Unused device retention90 days by last_seen_at, swept daily. A dead registration is deleted rather than retried.

Registration

The first signed-in launch asks the OS directly; no sheet explains the prompt first.
A grant registers the Expo push token. Every foreground return re-registers as a liveness heartbeat, and never re-asks.
A refusal is re-asked at the next launch, as often as the platform draws it; once it is permanent, the Settings switch routes into system settings.
Signing out and deleting the account both remove that device.

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

NameWherePurpose
APNs key, FCM V1 credentialsEAS, through eas credentialsSigning the pushes; never in your repo.
EXPO_ACCESS_TOKENThe backend's environmentOptional. Sends the fan-out as your Expo account.

On this page