6.6 KiB
TimeSafari — Native-First Notification System
Status: Ready for implementation
Date: 2025-09-07
Author: Matthew Raymer
Executive Summary
Ship a single, Native-First notification system: OS-scheduled background prefetch at T–lead + pre-armed local notifications. Web-push is retired.
What we deliver
- Closed-app delivery: Pre-armed locals fire even if the app is closed.
- Freshness: One prefetch attempt per slot at T–lead; ETag/TTL controls; skip when stale.
- Android precision: Exact alarms with permission; windowed fallback (±10m) otherwise.
- Resilience: Re-arm after reboot/time-change (Android receivers; iOS on next wake/silent push).
- Cross-platform: Same TS API (iOS/Android/Electron). Electron is best-effort while running.
Success signals
- High delivery reliability, minute-precision on Android with permission.
- Prefetch budget hit rate at T–lead; zero stale deliveries beyond TTL.
Strategic Plan
Goal
Deliver 1..M daily notifications with OS background prefetch at T–lead and rolling-window safety so messages display with fresh content even when the app is closed.
Tenets
- Reliability first: OS delivers once scheduled; no JS at delivery time.
- Freshness with guardrails: Prefetch at T–lead; enforce TTL-at-fire; ETag-aware.
- Single system: One TS API; native adapters swap under the hood.
- Platform honesty: Android exactness via permission; iOS best-effort budget.
Architecture (high level)
App (Vue/TS) → Orchestrator (policy) → Native Adapters:
- SchedulerNative — AlarmManager (Android) / UNUserNotificationCenter (iOS)
- BackgroundPrefetchNative — WorkManager (Android) / BGTaskScheduler (+ silent push) (iOS)
- DataStore — SQLite
Scheduling & T–lead
- Arm a rolling window (today + tomorrow within iOS cap).
- Attempt a single online-first fetch per slot at T–lead = T − prefetchLeadMinutes.
- If prefetch is skipped, the armed local still fires using cached content.
Policies
- TTL-at-fire: If (T − fetchedAt) >
ttlSeconds
→ skip arming. - Android exactness: Request
SCHEDULE_EXACT_ALARM
; fallback ±10m window. - Reboot/time change: Android receivers re-arm next 24h; iOS on next wake/silent push.
- No delivery-time mutation: iOS locals cannot be mutated by NSE; render before scheduling.
Implementation Guide
1) Interfaces (TS stable)
- SchedulerNative:
scheduleExact({slotId, whenMs, title, body, extra})
,scheduleWindow(..., windowLenMs)
,cancelBySlot
,rescheduleAll
,capabilities()
- BackgroundPrefetchNative:
schedulePrefetch(slotId, atMs)
,cancelPrefetch(slotId)
- DataStore: SQLite adapters (notif_contents, notif_deliveries, notif_config)
- Public API:
configure
,requestPermissions
,runFullPipelineNow
,reschedule
,getState
2) Templating & Arming
- Render
title/body
before scheduling; pass via SchedulerNative. - Route all arming through SchedulerNative to centralize Android exact/window semantics.
3) T–lead (single attempt)
- Compute T–lead =
whenMs - prefetchLeadMinutes*60_000
. BackgroundPrefetchNative.schedulePrefetch(slotId, atMs=T–lead)
.- On wake: ETag fetch (timeout 12s), persist, optionally cancel & re-arm if within TTL.
- Never fetch at delivery time.
4) TTL-at-fire
if (whenMs - fetchedAt) > ttlSeconds*1000 → skip
5) Android specifics
- Request
SCHEDULE_EXACT_ALARM
; deep-link if denied; fallback tosetWindow(start,len)
(±10m). - Receivers:
BOOT_COMPLETED
,TIMEZONE_CHANGED
,TIME_SET
→ recompute & re-arm for next 24h and schedule T–lead prefetch.
6) iOS specifics
BGTaskScheduler
for T–lead prefetch (best-effort). Optional silent push nudge.- Locals:
UNCalendarNotificationTrigger
(one-shots); no NSE mutation for locals.
7) Network & Timeouts
- Content fetch: 12s timeout; single attempt at T–lead; ETag/304 respected.
- ACK/Error: 8s timeout, fire-and-forget.
8) Electron
- Notifications while app is running; recommend Start-on-Login. No true background scheduling when fully closed.
9) Telemetry
- Record
scheduled|shown|error
; ACK deliveries (8s timeout); include slot/times/TZ/app version.
Capability Matrix
Capability | Android (Native) | iOS (Native) | Electron | Web |
---|---|---|---|---|
Multi-daily locals (closed app) | ✅ | ✅ | ✅ (app running) | — |
Prefetch at T–lead (app closed) | ✅ WorkManager | ⚠️ BGTask (best-effort) | ✅ (app running) | — |
Re-arm after reboot/time-change | ✅ Receivers | ⚠️ On next wake/silent push | ✅ Start-on-Login | — |
Minute-precision alarms | ✅ with exact permission | ❌ not guaranteed | ✅ timer best-effort | — |
Delivery-time mutation for locals | ❌ | ❌ | — | — |
ETag/TTL enforcement | ✅ | ✅ | ✅ | — |
Rolling-window safety | ✅ | ✅ | ✅ | — |
Acceptance Criteria
Core
- Closed-app delivery: Armed locals fire at T with last rendered content. No delivery-time network.
- T–lead prefetch: Single background attempt at T–lead; if skipped, delivery still occurs from cache.
- TTL-at-fire: No armed local violates TTL at T.
Android
- Exact permission path: With
SCHEDULE_EXACT_ALARM
→ within ±1m; else ±10m window. - Reboot recovery: After reboot, receivers re-arm next 24h and schedule T–lead prefetch.
- TZ/DST change: Recompute & re-arm; future slots align to new wall-clock.
iOS
- BGTask budget respected: Prefetch often runs but may be skipped; delivery still occurs via rolling window.
- Force-quit caveat: No background execution after user terminate; delivery still occurs if pre-armed.
Electron
- Running-app rule: Delivery only while app runs; with Start-on-Login, after reboot the orchestrator re-arms and subsequent slots deliver.
Network
- Content fetch timeout 12s; ACK/Error 8s; no retries inside lead; ETag honored.
Observability
- Log/telemetry for
scheduled|shown|error
; ACK payload includes slot, times, device TZ, app version.
Web-Push Cleanup
Web-push functionality has been retired due to unreliability. All web-push related code paths and documentation sections should be removed or marked as deprecated. See web-push-cleanup-guide.md
for detailed cleanup steps.
This document consolidates the Native-First notification system strategy, implementation details, capabilities, and acceptance criteria into a single comprehensive reference.