System design
Part 2 of 6 · Push NotificationsiOS Notification Service Extension — Rich Content, Mute Rules & Time Budgets
NSE runs briefly before iOS presents a remote notification. You get a short wall-clock budget to mutate title/body/attachments or apply mute rules. Timeout or crash fail-open: the user still sees the original payload.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Where rich content is decided
Prefer
Thin APNs payload + best-effort NSE enricher + server-side mute
The banner text is useful without the image. NSE fetches a thumbnail under a deadline and always calls contentHandler once. Quiet hours are enforced before send.
- mutable-content is a hint, not a guarantee the extension will finish.
- Signed media URL TTL exceeds the NSE budget but stays short for privacy.
- Measure enrich_ok / enrich_timeout / enrich_error — no PII.
Alternative
Fat payload, or NSE as the compliance engine
Inline base64, long downloads, or 'the extension will suppress marketing' fail in production. Timeouts drop attachments; some system paths still surface. Abuse analytics if you counted sent but the user never should have been targeted.
- NSE cannot mint a Critical Alert entitlement.
- Auth-expired media URLs look like 'rich push is broken'.
- Over-fetch (full video) burns battery for a shade thumbnail.
NSE in the pipeline
Fail-open is the contract. Interviews fail people who assume the image always lands.
- 1
APNs delivers mutable-content
Remote push with mutable-content: 1. OS launches the service extension. - 2
Start a wall-clock deadline
Treat OS budget as short. Soft stop earlier (e.g. 20s). Parallel small fetches; cancel on deadline. - 3
Mutate or suppress
UNMutableNotificationContent: title, body, subtitle, userInfo, attachments. Mute from on-device prefs — no network if you can avoid it. - 4
Always call contentHandler once
Timeout or crash: OS presents unmodified payload (attachments may be missing). - 5
Present in shade
Useful without enrichment. Thread-id groups the conversation or order.
Overview
NSE runs briefly before the system presents a remote notification. Interviewers probe the time budget, what you can mutate, mute/suppress rules, and the failure mode: the user still sees the original payload.
Not for: long downloads, auth UI, or replacing your entire backend compose path.
You should be able to:
- Walk mutable-content → launch → deadline → contentHandler.
- Refuse NSE as a substitute for critical alerts.
- Say what happens if the extension is killed in a loop (OS may throttle).
NSE vs Content Extension vs AppDelegate
| Piece | Job | Not for |
|---|---|---|
| NSE (Service) | Pre-display mutate; download attachment; decrypt a thin payload | Custom expanded UI; long work |
| Content Extension | Custom UI when the user long-presses / expands | Pre-display mutate |
| App / SceneDelegate | Open & actions after the user engages | Anything before the banner exists |
Architecture (timeout is a branch, not a self-loop)
A labeled self-loop on NSE collides with the contentHandler edge. Use a deadline decision so desktop labels stay readable.
Decisions
- 1
1 APNs mutable-content
- next2 Notification delivery
- 2
2 Notification delivery
- next3 Launch NSE
- 3
3 Launch NSE
- next4 Deadline ok?
- ?
4 Deadline ok?
- yes5 Fetch media / apply mute
- no5 Timeout: original content
- 5
5 Fetch media / apply mute
- next6 contentHandler once
- 6
5 Timeout: original content
- next6 contentHandler once
- 7
6 contentHandler once
- next7 Banner / shade
- 8
7 Banner / shade
Diagrams - step by step
Three small diagrams for the Notification Service Extension. Step numbers in the labels give the animation order. The lesson map under Diagram 1 plays those steps.
Diagram 1 - Happy path: mutate before display
Flow
- 1
Step 1 Server sends a thin alert with mutable-content 1
- nextStep 2 APNs delivers to the device
- 2
Step 2 APNs delivers to the device
- nextStep 3 iOS launches the Service Extension
- 3
Step 3 iOS launches the Service Extension
- nextStep 4 Fetch media or decrypt within the time budget
- 4
Step 4 Fetch media or decrypt within the time budget
- nextStep 5 Mutate title, body, attachments
- budget runs outFailure path - iOS shows the original payload
- 5
Step 5 Mutate title, body, attachments
- nextStep 6 Call contentHandler
- 6
Step 6 Call contentHandler
- nextStep 7 iOS presents the banner
- 7
Step 7 iOS presents the banner
- 8
Failure path - iOS shows the original payload
The extension only runs for alerts with mutable-content set, and only for a short budget (roughly 30 s, treat it as less). Whatever it does, the system falls back to the original content if it crashes or runs out of time.
Lesson map
iOS Notification Service Extension — Rich Content, Mute Rules & Time Budgets
Diagram 1 walks 7 steps from Step 1 Server sends a thin alert with mutable-content 1 through Step 7 iOS presents the banner.
Architecture. Step 1 Server sends a thin alert with mutable-content 1 Ready. Step 2 APNs delivers to the device Ready. Step 3 iOS launches the Service Extension Ready. Step 4 Fetch media or decrypt within the time budget Ready. Step 5 Mutate title, body, attachments Ready. Step 6 Call contentHandler Ready. Step 7 iOS presents the banner Ready. Failure path - iOS shows the original payload Ready
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB A["Step 1 Server sends a thin alert with mutable-content 1 Ready"] B["Step 2 APNs delivers to the device Ready"] C["Step 3 iOS launches the Service Extension Ready"] D["Step 4 Fetch media or decrypt within the time budget Ready"] E["Step 5 Mutate title, body, attachments Ready"] F["Step 6 Call contentHandler Ready"] G["Step 7 iOS presents the banner Ready"] X["Failure path - iOS shows the original payload Ready"] A -->|continues| B B -->|continues| C C -->|continues| D D -->|continues| E E -->|continues| F F -->|continues| G D -->|budget runs out| X
Diagram 2 - Failure path: big download blows the time budget
Sequence
- 1
APNs → iOS
Step 1 alert with mutable-content 1
- 2
iOS → NSE
Step 2 launch the extension
- 3
NSE → Media CDN
Step 3 download a 40 MB video attachment
- 4
Media CDN → NSE
Step 4 slow network, download still running
- 5
iOS → NSE
Step 5 serviceExtensionTimeWillExpire
- 6
NSE → iOS
Step 6 no time left - original text, no attachment
- 7
APNs
Fix - small thumbnails, short timeouts, always call contentHandler with a fallback
Long downloads are the classic NSE failure. Design for fail-open: the user still gets the original text, so keep it meaningful, and measure how often the fallback fires.
Diagram 3 - Decision: NSE vs Content Extension vs app delegate
Decisions
- ?
Step 1 What do you need?
- change content before displayNotification Service Extension
- custom UI on long pressNotification Content Extension
- handle open or actionApp or SceneDelegate
- 2
Notification Service Extension
- nextStep 2 Work fits in a few seconds?
- Wrong pick as the compliance gateMute rules must run on the server before send
- 3
Notification Content Extension
- 4
App or SceneDelegate
- ?
Step 2 Work fits in a few seconds?
- yesFetch a thumbnail or decrypt a thin payload
- noDo it server side or after open
- 6
Fetch a thumbnail or decrypt a thin payload
- 7
Do it server side or after open
- 8
Mute rules must run on the server before send
The Service Extension mutates before display, the Content Extension draws expanded UI, and the app handles taps. Server-side preferences are the real mute gate; the NSE is only a last-chance tweak.
Mute rules (server + client)
- Server — respect user prefs before send (quiet hours, category opt-out). Counting "sent" for a user who opted out poisons analytics.
- NSE — last-chance suppress: call contentHandler with empty / modified content; set interruption level carefully.
- Abuse — never rely on NSE alone for compliance; some system paths still surface.
If the app is foreground with a live session, suppressing the OS banner in favor of in-app UI is a product policy — see WebSockets & MQTT.
Time-budget patterns
- Start a wall-clock deadline immediately (soft ~20s / OS hard stop).
- Prefer parallel small fetches; cancel on deadline; always call contentHandler once.
- Cache category mute prefs on-device so suppress does not need the network.
- Signed URL TTL should exceed the NSE budget but stay short for privacy.
- Emit a local metric:
enrich_ok|enrich_timeout|enrich_error(aggregated, no PII).
Payload hygiene
- Opaque
nid+ media token — not email/phone inaps.alert. - Localization: prefer
loc-key/ localized strings when possible. - Size: keep the remote payload small; attachments via URL, not inline base64.
- Threading: stable
thread-idper conversation/order for shade grouping. - Headers:
apns-push-type,apns-priority,apns-collapse-id,apns-topic.
Sandbox: NSE decision (TypeScript)
Conceptual planner. Real NSE is Swift; this encodes the policy so you can defend it on a whiteboard.
Press Run. Snippets must be self-contained — no network, files, or native modules.
Same idea (Python): compose flags for APNs
Keep PII out. Media is an opaque URL the NSE may fetch. from __future__ import annotations so str | None parses.
Press Run. Snippets must be self-contained — no network, files, or native modules.
Failure modes
- NSE timeout — rich image missing; text still shows; measure enrich success rate.
- Extension crash loops — OS may throttle; ship defensive NSE with hard deadlines.
- Auth-expired media URLs — attachment fail; short-lived signed URLs + fallback text.
- Over-fetch — battery/network; thumbnail CDN, not full video.
- Assuming suppress always works — some system paths still surface; server-side mute first.
Pitfalls
Sketch NSE with a 25s image fetch against a ~30s budget. What does the user see? What metric fires? Now mute the category on-device with no network — what does contentHandler receive?
Interview Q&A
What does mutable-content do?
Answer
It signals iOS to run the Notification Service Extension before presentation so you can mutate content or attach media.
What if NSE exceeds the budget?
Answer
The OS presents the original payload. Attachments may be absent. Measure timeout vs success; do not pretend every send was rich.
Can NSE replace Critical Alert entitlements?
Answer
No. Critical needs a capability plus user permission. NSE cannot invent that. Depth: critical alerts.
Why thin payloads?
Answer
APNs size limits and privacy. NSE or the app fetches details with a real session. Banner text must still make sense alone.
NSE vs Content Extension?
Answer
Service mutates before show. Content customizes the expanded UI after the user interacts.
How do you test timeout paths?
Answer
Inject a slow URL. Assert fail-open presentation and an enrich_timeout metric. Never block the handler on a retry storm.
What is thread-id for?
Answer
Groups notifications in the shade (conversation or order thread) so collapse and visual stacking match the product.
Foreground app with a live socket?
Answer
You may suppress the OS banner and render in-app. That is a policy choice on top of WebSockets & MQTT, not an NSE feature.
Who owns apns-collapse-id vs nid?
Answer
Collapse coalesces pending shade rows. nid is analytics and idempotency identity. Depth: FCM collapse and reliability.
Should NSE download the full video?
Answer
No. Thumbnail CDN. Full media after open. Over-fetch is a battery and timeout bug.