# 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.*
