# TimeSafari — Native-First Notification System **📝 SANITY CHECK IMPROVEMENTS APPLIED:** This document has been updated to clarify current background fetch infrastructure status and iOS implementation completeness. **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** *(see Glossary)* and **rolling-window safety** *(see Glossary)* 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** *(see Glossary)*; enforce **TTL-at-fire** *(see Glossary)*; ETag-aware. - **Single system:** One TS API; native adapters swap under the hood. - **Platform honesty:** Android exactness via permission; iOS best-effort budget. - **No delivery-time network:** Local notifications display **pre-rendered content only**; never fetch at delivery. ### Architecture (high level) App (Vue/TS) → Orchestrator (policy) → Native Adapters: - **SchedulerNative** — AlarmManager (Android) / UNUserNotificationCenter (iOS) - **BackgroundPrefetchNative** — WorkManager (Android) / BGTaskScheduler (+ silent push) (iOS) - **DataStore** — SQLite **Storage:** **Current:** SharedPreferences (Android) / UserDefaults (iOS) + in-memory cache (see Glossary → Tiered Storage). **Planned:** one **shared SQLite** file (see Glossary → Shared DB); the app owns schema/migrations; the plugin opens the same path with **WAL** (see Glossary → WAL); background writes are **short & serialized**. *(Keep the "(Planned)" label until Phase 1 ships.)* ### SQLite Ownership & Concurrency *(Planned)* * **One DB file:** The plugin will open the **same path** the app uses (no second DB). * **Migrations owned by app:** The app executes schema migrations and bumps `PRAGMA user_version` (see Glossary → PRAGMA user_version). The plugin **never** migrates; it **asserts** the expected version. * **WAL mode:** Open DB with `journal_mode=WAL`, `synchronous=NORMAL`, `busy_timeout=5000`, `foreign_keys=ON`. WAL (see Glossary → WAL) allows foreground reads while a background job commits quickly. * **Single-writer discipline:** Background jobs write in **short transactions** (UPSERT per slot), then return. * **Encryption (optional):** If using SQLCipher, the **same key** is used by both app and plugin. Do not mix encrypted and unencrypted openings. *Note: Currently using SharedPreferences (Android) / UserDefaults (iOS) with in-memory cache. See Implementation Roadmap → Phase 1.1 for migration steps.* ### Scheduling & T–lead *(Planned)* - Arm **rolling window** (see Glossary → Rolling window). **(Planned)** - Exactly **one** online-first fetch at **T–lead** (see Glossary → T–lead) with **12s** timeout; **ETag/304** respected. **(Planned)** - If fresh **and** within **TTL-at-fire** (see Glossary → TTL), (re)arm; otherwise keep prior content. **(Planned)** - If the OS skips the wake, the pre-armed local still fires from cache. **(Planned)** *Note: Current implementation has basic scheduling and WorkManager infrastructure (Android) but lacks T–lead prefetch logic, rolling window logic, and iOS background tasks. See Implementation Roadmap → Phase 1-2.* ### Policies *(Mixed Implementation)* - **TTL-at-fire** (see Glossary → TTL): Before arming for T, if `(T − fetchedAt) > ttlSeconds` → **skip**. **(Planned)** - **Android exactness** (see Glossary → Exact alarm): **(Partial)** — permission flow exists; finalize ±10m window & deep-link to settings. - **Reboot/time change:** **(Partial)** — Android receivers partially present; iOS via next wake/silent push. - **No delivery-time network** (see Glossary → No delivery-time network): Local notifications display **pre-rendered content only**; never fetch at delivery. **(Implemented)** --- ## 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` ### DB Path & Adapter Configuration *(Planned)* * **Configure option:** `dbPath: string` (absolute path or platform alias) will be passed from JS to the plugin during `configure()`. * **Shared tables:** * `notif_contents(slot_id, payload_json, fetched_at, etag, …)` * `notif_deliveries(slot_id, fire_at, delivered_at, status, error_code, …)` * `notif_config(k, v)` * **Open settings:** * `journal_mode=WAL` * `synchronous=NORMAL` * `busy_timeout=5000` * `foreign_keys=ON` *Note: Currently using SharedPreferences/UserDefaults (see Glossary → Tiered Storage). Database configuration is planned for Phase 1.* See **Implementation Roadmap → Phase 1.1** for migration steps and schema. **Type (TS) extension** *(Planned)* ```ts export type ConfigureOptions = { // …existing fields… dbPath: string; // shared DB file the plugin will open storage: 'shared'; // canonical value; plugin-owned DB is not used }; ``` *Note: Current `NotificationOptions` interface exists but lacks `dbPath` configuration. This will be added in Phase 1.* **Plugin side (pseudo)** ```kotlin // Android open val db = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READWRITE) db.execSQL("PRAGMA journal_mode=WAL") db.execSQL("PRAGMA synchronous=NORMAL") db.execSQL("PRAGMA foreign_keys=ON") db.execSQL("PRAGMA busy_timeout=5000") // Verify schema version val uv = rawQuery("PRAGMA user_version").use { it.moveToFirst(); it.getInt(0) } require(uv >= MIN_EXPECTED_VERSION) { "Schema version too old" } ``` ```swift // iOS open (FMDB / SQLite3) // Set WAL via PRAGMA after open; check user_version the same way. ``` ### 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) **T–lead governs prefetch, not arming.** We **arm** one-shot locals as part of the rolling window so closed-app delivery is guaranteed. At **T–lead = T − prefetchLeadMinutes**, the **native background job** attempts **one** 12s ETag-aware fetch. If fresh content arrives and will not violate **TTL-at-fire**, we (re)arm the upcoming slot; if the OS skips the wake, the pre-armed local still fires with cached content. - 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 **TTL-at-fire:** Before arming for time **T**, compute `T − fetchedAt`. If that exceeds `ttlSeconds`, **do not arm** (skip). This prevents posting stale notifications when the app has been closed for a long time. `if (whenMs - fetchedAt) > ttlSeconds*1000 → skip` ### 5) Android specifics - Request `SCHEDULE_EXACT_ALARM`; deep-link if denied; fallback to `setWindow(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. ### DB Sharing *(Planned)* * **Shared DB visibility:** A background prefetch writes `notif_contents`; the foreground UI **immediately** reads the same row. * **WAL overlap:** With the app reading while the plugin commits, no user-visible blocking occurs. * **Version safety:** If `user_version` is behind, the plugin emits an error and does not write (protects against partial installs). *Note: Currently using SharedPreferences/UserDefaults with in-memory cache. SQLite sharing is planned for Phase 1.* --- ## 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._