Mobile Push Notification Preference Spec
Push preferences look like a settings screen and turn out to be a distributed system. I have watched teams ship a single "notifications" toggle, then spend the next two years untangling why a security alert got silenced along with a marketing ping. This is the spec I wish those teams had written on day one.
Review Note
Reviewed May 6, 2026. This focused reference is now promoted as a search-indexable companion to the API Contracts Hub. It includes concrete review artifacts, failure modes, and next-step links for readers applying the topic in practice.
Category Model Beats Channel Model, and It Is Not Close
The first real decision is how you group notifications. Teams keep modeling preferences by channel — sound, badge, lock screen — because that is what the OS settings app surfaces. Do not do that. Channel is a rendering concern. Category is a consent concern, and consent is the thing your product actually needs to reason about.
Write the category taxonomy as a closed enum. I use four buckets: transactional (order shipped, payment received), security (new device sign-in, 2FA code), social (likes, comments, mentions), and marketing (announcements, promos, re-engagement). If a proposed notification does not fit, the spec forces the question: is this really a new category, or are you sneaking marketing into transactional so more users see it?
Picture a photo-sharing app. Someone likes my post — social. Someone signs in from a new country — security. A Black Friday promo — marketing. The order confirmation for a print I bought — transactional. Same user, same device, four categories, four independent opt-in states. Default state differs per category: security defaults on and is very hard to turn off, transactional defaults on, social defaults on but is one tap to mute per sub-type, and marketing must default off wherever GDPR applies with opt-in explicit, logged, and revocable. One global push toggle makes all of that impossible.
The OS Permission Dance Must Be Written Down
On iOS, I want provisional authorization (UNAuthorizationOptionProvisional) for anything that can deliver quietly to Notification Center without interrupting the user — it lets us demonstrate value before the hard prompt. On Android 13 and above, POST_NOTIFICATIONS is a runtime permission, and the spec must say when we ask.
My rule: never prompt on first launch. Prompt at the first moment a notification would create obvious value — after a user's first photo, or right after enabling two-factor auth. Log a permission_prompt_shown event and never prompt twice. If the user denies, route them to an in-app explanation with a deep link to OS settings, not a repeated native prompt.
Per-Category State Is a Tiny State Machine
Per category, per user, per device, the preference has three states: default, user-on, and user-off. The distinction between default-on and user-on controls what happens when you later change the default. If a category was user-on, a server-side default flip does not touch it. If it was default-on, the new default applies. Without this, every default change silently overwrites real user choices.
Store the transition timestamp and source (in_app_settings, os_settings_sync, unsubscribe_link, admin_override). You will want that audit trail the first time legal asks whether a user actually consented to marketing on 2025-11-03.
Quiet Hours: Store the Timezone, Not the Offset
"9pm to 8am" is meaningless without a timezone, and a timezone is not a UTC offset. If I set quiet hours in New York and fly to Tokyo, do I want 9pm Tokyo or 9pm New York? My answer: quiet hours follow the device's current IANA zone. Store start and end as local wall-clock times plus an IANA zone id refreshed on every app foreground.
The spec must also say what "quiet" means. My default: suppress marketing and social, delay-deliver transactional until the window ends, bypass entirely for security. Every spec needs an explicit rule per category, not a generic "respect quiet hours" line.
Device-Level vs Account-Level: Most Restrictive Wins
A user has an account-level preference and each device has its own OS permission. The conflict rule is "most restrictive wins, and the user can always see why a notification did not arrive." If the account opts out of social but the iPhone permission is on, social is still suppressed. If the account opts in to marketing but Android POST_NOTIFICATIONS is denied, the settings screen shows "blocked at the OS level" rather than a misleading green toggle.
Critical Alerts Need Gatekeepers, Not PM Discretion
iOS critical alerts bypass Do Not Disturb. Android's time-sensitive and full-screen-intent categories do similar things. The spec must name who can flag a notification as critical, and the answer should almost never be the PM who owns the feature. I write it as: only Security can request critical-alert delivery, and only a named list (suspicious_login, account_takeover_blocked, 2fa_code) is eligible. Anything else gets denied at the compose layer, not discovered in review.
Delivery Receipts Lie, and the Spec Should Admit It
APNs and FCM tell you a message was accepted by their servers. They will not reliably tell you it hit the lock screen. Battery-saver mode, OEM notification throttling on Android skins, dwell-time limits — all silently drop or delay messages. State what "delivered" actually measures (accepted by the push provider) and define a separate user_observed event that fires on tap or badge clear. Do not let dashboards promise guarantees the platform cannot back up.
GDPR, TCPA, and the SMS Fallback Trap
Marketing pushes to EU users need a lawful basis, and "they installed the app" is not it. Require explicit opt-in with a timestamped consent record before any marketing push to a device whose last known region is EEA or UK. Transactional and security ride on legitimate interest and contractual necessity — but the spec must say so explicitly so legal can review.
The sneaky one is SMS fallback. When push fails, teams reach for SMS. That crosses into TCPA territory, and push consent does not cover SMS. The spec either forbids SMS fallback for marketing entirely, or requires a separate, documented SMS opt-in with its own audit trail.
Acceptance Criteria
- Given a user in Germany with no prior marketing opt-in When the marketing service attempts to send a promo push Then the send is rejected at the compose layer with reason "no_gdpr_consent" And no delivery attempt is made to APNs or FCM - Given a user with quiet hours 21:00 to 08:00 in America/New_York When a social notification is generated at 23:30 New York time Then the notification is suppressed and logged as "quiet_hours_dropped" And a security notification in the same window delivers with critical-alert flag - Given a user who toggled marketing to user-off on device A When the server changes the marketing category default from off to on Then device A remains user-off and no delivery occurs And the preference audit log retains the original user-off transition - Given an Android 13 device where POST_NOTIFICATIONS is denied When the in-app settings screen renders Then all category toggles display a "blocked at OS level" state And tapping any toggle deep-links to the system app settings
The Unsubscribe Flow Push Apps Keep Forgetting
CAN-SPAM does not apply to push, but a user who cannot easily mute a category will mute your whole app at the OS level, and you will never get them back. The spec requires a one-tap unsubscribe link inside every marketing and social push — tap it, land on a screen that confirms the category was muted, offer a single "undo" and a link to full preferences. No login wall. No "sad to see you go" friction. That one flow has done more for retention on products I have worked on than any re-engagement campaign.
Preference Drift Audit
Push preferences drift because three systems can disagree: the app preference store, the notification provider, and the operating system. The spec should include a weekly audit job, not just a settings screen.
Preference drift audit Input: - user preference snapshot - provider subscription state - iOS or Android permission state - last delivery receipt Mismatch examples: - app says marketing off, provider subscription still active - app says enabled, OS permission denied - critical alert enabled without entitlement - quiet hours missing timezone Action: - repair provider state when app preference is authoritative - show blocked-at-OS badge when OS denies delivery - suppress delivery when precedence is ambiguous - log audit repair with category, device id, and old/new state
This is not just hygiene. It prevents the most frustrating push bug: a user turns something off, the UI agrees, and the backend still sends it two days later.
Topic Path
Keep Reading
Editorial Note
- Author details: Spec Coding Editorial Team
- Editorial policy: How we review and update articles
- Corrections: Contact the editor