ADR-0044: Push notifications — FCM + APNs with sync-on-push and deep-link routing
Status
Accepted (pending FieldForce Phase 1 implementation)Tags
fieldforce, digital-worker, mobile, flutter, notifications, fcm, apns, deep-link, background-execution, powersyncDecision
Push notifications use FCM (Android) + APNs (iOS), handled in acore_notifications package, as the reliable channel to reach a backgrounded or killed app (which a persistent sync connection cannot).
- Normal pushes for user-facing events (work-order assignment, new commitment, etc.).
- Silent / background pushes trigger a PowerSync sync via a headless handler so data is fresh before the user opens the app — best-effort only (iOS throttles silent pushes; never relied on for correctness, only freshness).
- Deep-link routing: a push payload carries a deep-link target; on tap, the handler resolves payload → deep link → go_router navigation (e.g.
/work-orders/{id}). - Sync-on-push: a data-change push signals “new data exists” → the handler triggers a PowerSync sync.
- The full path (foreground / background / killed) is prototyped early.
Why
OS background limits mean a live PowerSync (or any) connection is suspended when the app is backgrounded — so the app cannot rely on a persistent connection to learn about server-side events while asleep. Push is the OS-sanctioned channel that reaches a suspended/killed app. Pairing push with PowerSync’s reconnect-and-reconcile model (ADR-0040) gives the complete realtime story: foreground = live sync; background = push notify/wake; resume = reconcile. The must-record nuance is the normal-vs-silent split and the best-effort contract. Normal pushes are user-facing and reliable; silent pushes are an optimization to pre-sync data before open, but iOS rate-limits and may drop them — so silent pushes must be treated as best-effort and never as a correctness mechanism. A future implementer must not build logic that depends on a silent push arriving. The reliable guarantees are: normal push reaches the user; PowerSync reconciles on resume regardless of whether a silent push fired. Routing is wired so a notification deep-links to the exact relevant screen rather than dumping the user on home — the payload→deep-link→go_router path is the contract. Rejected alternatives:- Keep a persistent connection alive in the background to receive events. Fights OS background limits; unreliable and battery-hostile. Rejected — push is the correct wake channel.
- Silent pushes as a guaranteed delivery mechanism. iOS throttles/drops them. Rejected — best-effort only.
- Notifications that always open the home screen (no deep links). Poor UX for “open this work order.” Rejected — payloads carry deep-link targets.
- A non-Firebase push stack. Firebase is already present (Crashlytics, FCM); adding a separate push provider is needless surface. Rejected.
Consequences
- Firebase is a dependency (already present for Crashlytics); APNs requires certificates/keys, capabilities, and entitlements — a known iOS setup cost prototyped early.
- The headless background handler must do two jobs cleanly: trigger PowerSync sync on data-change signals, and resolve deep links on tap.
- Silent-push freshness is best-effort; correctness always falls back to PowerSync resume-reconcile.
core_notificationsdepends oncore_database(to trigger sync) and the routing layer (for deep links) — consistent with the dependency DAG (ADR-0039).
Rules for agents
- Use FCM + APNs via
core_notifications. Normal pushes for user-facing events; silent pushes are best-effort pre-sync only. - NEVER build logic that depends on a silent push arriving — correctness comes from PowerSync resume-reconcile.
- Push payloads carry a deep-link target; on tap, route via go_router. A data-change push triggers a PowerSync sync.
- Prototype foreground/background/killed paths early.
- Do NOT attempt to keep a sync connection alive in the background; rely on push + resume.