Push notifications that respect the user: tiering, quiet hours, and deep linking

Notification permission is a finite resource that most apps spend recklessly

When a user grants notification permission, the app gets one of the most valuable channels in mobile. When the user revokes it — and revocation is one OS settings tap away on both platforms — the channel is gone, often permanently. Most apps treat the permission as if it's renewable, fire notifications for every minor event, train the user to mute or revoke, and then wonder why retention slipped.

Operator apps live or die by this discipline. A field-service tech who mutes the operator app because it sent twelve low-priority pings during dinner won't see the genuine dispatch the next morning. The notification architecture has to assume permission is precious from the first notification onward.

Tiered severity is the foundation of a notification policy that holds up

We design every operator app with three severity tiers. Critical: deliver always, override quiet hours where appropriate (and platform allows), interrupt with sound, deep-link to the action surface. Important: deliver with sound during work hours, silent or batched during quiet hours, deep-link to the action surface. Informational: deliver silently, batch into a daily digest where possible, deep-link if there's an action.

Each notification has to declare its tier at the source. Backend code that fires push notifications has to specify which tier it falls into — and the notification service rejects notifications that don't. The discipline forces the engineering team to think about whether each notification deserves attention before they ship the code.

Severity tiers
3 critical / important / informational
Notification opt-out rate
< 4% over 6 months
Critical-tier delivery latency
< 8s from event to device
Deep-link success rate
> 99% lands on intended surface

Quiet hours are per recipient, not per app

A naive quiet-hours implementation hardcodes 9pm-7am quiet for the whole app. Reality: operators work shifts that don't align with consumer time. A field tech on the second shift wants notifications at 11pm and would prefer not to be woken at 7am for non-critical pings. Quiet hours have to be configurable per recipient, with sensible defaults the user can override.

We surface quiet-hour configuration in the app's settings, default to the user's working hours where the system knows them (calendar integration on supported platforms), and propagate the configuration to the notification backend. The backend evaluates quiet hours per recipient at send time — not at compose time — so a notification that bounces around in retry doesn't deliver outside the recipient's quiet window.

APNs and FCM have meaningfully different delivery semantics

Apple's APNs and Google's FCM look similar at API level and behave differently in production. APNs has a strict priority system (10 = immediate, 5 = power-aware), strict TTL handling, and aggressive coalescing at the device level. FCM has different priority semantics ('high' versus 'normal'), longer default TTL, and less aggressive coalescing. The same payload sent to APNs and FCM with the same notion of 'important' will deliver differently.

Our notification service speaks the platform-specific dialect from the source. A 'critical' tier on iOS uses APNs priority 10 with a short TTL; on Android, FCM 'high' priority with the same TTL. Our code path is platform-aware rather than abstracted-and-leaky. Treating both providers as one is how delivery gets weird in ways that take weeks to debug.

A notification that taps to the home screen of the app is a context switch with extra steps. The user has to navigate to find the relevant surface, often forgetting why they tapped in the first place. A notification that deep-links to the exact surface — the dispatch detail, the message thread, the alert page — is an action shortcut. Same data, very different experience.

We architect deep-linking with universal links on iOS and App Links on Android, signed with the customer's domain, and a routing layer in the app that resolves the link to the right native surface. When the app is cold-started by a notification tap, the router has to handle the cold-start case (auth not yet loaded, data not yet fetched) and end up on the right surface anyway. This is more engineering than people expect.

Notification analytics are how the team knows the policy is working

Every notification logs delivery, suppression (quiet hours, tier policy), tap rate, and follow-on action. Aggregations roll up per tier, per category, per recipient cohort. The team sees: critical-tier tap rate 91%, important-tier 64%, informational-tier 22%. Important-tier tap rate dropping below 50% over a month is a signal that the tier is being overused and needs tightening.

Without analytics, notification policy drifts toward over-firing. With analytics, the team has empirical evidence that a category is wearing out its welcome and needs to demote to informational or stop firing entirely. The metric is the discipline.

Notification permission flow is a UX decision, not a default

iOS requires explicit user opt-in for notifications. Android 13+ requires the same. The default flow — request permission on first launch with a generic 'Allow notifications?' system prompt — produces the worst opt-in rates and trains the user to think the app is rude. The right pattern is a pre-prompt that explains what the user gets, requests permission only after the user has experienced the value, and makes the cost (notification frequency) explicit.

Operator apps that get this right see opt-in rates above 90% on initial enrollment. Operator apps that ask for permission badly see opt-in below 60% — which means the app's notification channel is half-broken from day one, and recovering after the first refusal is hard.

We had a 38% notification opt-out rate after six months on the previous architecture. Tiered severity, per-user quiet hours, and proper deep linking pulled that down to 4%. The technicians stopped muting us because we stopped pinging them for things that didn't matter. The retention math on that change paid for the rework four times over.

— VP Engineering, field-service mobile deployment

Frequently asked

Why does notification design matter so much for operator apps?

Because notification permission is finite. Once a user mutes the app or revokes notification permission — both one settings tap away on iOS and Android — the channel is gone, often permanently. An operator who mutes the app because it pinged twelve times during dinner won't see the genuine dispatch the next morning. The architecture has to treat permission as precious from the first notification onward.

What are notification severity tiers?

Three tiers we standardize on. Critical: deliver always, override quiet hours where allowed, interrupt with sound, deep-link to action. Important: deliver with sound during work hours, batched or silent during quiet hours, deep-link to action. Informational: deliver silently, batch into a digest where possible, deep-link if applicable. Every notification declares its tier at the source — backend code that doesn't specify a tier is rejected by the notification service.

How are quiet hours configured?

Per recipient, not per app. Operators work shifts that don't align with consumer time, so a hardcoded 9pm-7am quiet window is wrong for many of them. Defaults adapt to the user's working hours where the system knows them; users can override in settings; the configuration propagates to the notification backend, which evaluates quiet hours per recipient at send time.

Are APNs and FCM interchangeable?

No. APNs has strict priority levels (10 = immediate, 5 = power-aware), strict TTL handling, aggressive device-level coalescing. FCM has different priority semantics ('high' / 'normal'), longer default TTL, less aggressive coalescing. Our notification service speaks the platform-specific dialect from the source. Treating both providers as one is how delivery gets weird in ways that take weeks to debug.

What does proper deep linking require?

Universal links on iOS and App Links on Android, signed with the customer's domain. A routing layer in the app that resolves the link to the right native surface, including handling cold-start cases where the app launches in response to the notification tap with auth and data not yet loaded. Universal links and App Links must be configured at domain level (apple-app-site-association, assetlinks.json), which means the customer's domain configuration is part of the integration.

How is notification policy measured over time?

Per-tier and per-category tap rates, suppression counts, opt-out rates, and follow-on action conversion. Important-tier tap rate dropping below 50% is a signal the tier is over-used and needs tightening. Operator apps that get the architecture right see opt-out under 5% over six months. Operator apps that don't see opt-out climb past 25% within months and the channel is effectively dead before the team realizes it.