You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

25 KiB

TimeSafari Notification System — Strategic Plan

Status: 🚀 Active plan Date: 2025-09-05T05:09Z (UTC) Author: Matthew Raymer Scope: v1 (in‑app orchestrator) now; path to v2 (native plugin) next Goal: We will deliver 1..M local notifications/day with content prefetched so messages will display offline. We will support online‑first (API→DB→Schedule) with offline‑first fallback. The system will enhance TimeSafari's community-building mission by keeping users connected to gratitude, gifts, and collaborative projects through timely, relevant notifications.

Implementation Details: See notification-system-implementation.md for detailed code, database schemas, and integration specifics. Canonical Ownership: This document owns Goals, Tenets, Platform behaviors, Acceptance criteria, and Test cases.


1) Versioning & Intent

  • v1 (In‑App Orchestrator): We will implement multi‑daily local notifications, online/offline flows, templating, SQLite persistence, and eventing inside the app using Capacitor Local Notifications.
  • v2 (Plugin): We will extract adapters to a Capacitor/Native plugin to gain native schedulers (WorkManager/AlarmManager; BGTask+UNUserNotificationCenter), native HTTP, and native SQLite with the same TypeScript API.

We will retain the existing web push + Service Worker foundation; the system will add reliable local scheduling on mobile and a unified API across platforms.


2) Design Tenets

  • Reliability: OS‑level delivery once scheduled; no reliance on JS being alive at fire time.
  • Freshness: Prefer online‑first within a short prefetch window; degrade gracefully to cached content with TTL.
  • Extractable: Clean interfaces (Scheduler, DataStore, Callbacks) so v2 will swap adapters without API changes.
  • Simplicity: One‑shot notifications per slot; rolling window scheduling to respect platform caps.
  • Observability: Persist deliveries and errors; surface minimal metrics; enable ACKs.
  • Privacy-First: Follow TimeSafari's privacy-preserving architecture; user-controlled visibility and data sovereignty.
  • Community-Focused: Enhance TimeSafari's mission of connecting people through gratitude, gifts, and collaborative projects.

3) Architecture Overview

Application (Vue/TS)
 ├─ NotificationOrchestrator (core state)
 │   ├─ Scheduler (adapter)
 │   ├─ DataStore (adapter)
 │   └─ Callbacks (adapter)
 └─ UI (settings, status)

Adapters
 ├─ V1: SchedulerCapacitor, DataStoreSqlite, CallbacksHttp
 └─ V2: SchedulerNative, DataStoreNativeSqlite, CallbacksNativeHttp

**Scheduler Adapter:** All notification arming must go through the Scheduler adapter to honor platform timing semantics (exact alarms vs. windowed fallback).

Platform
 ├─ iOS/Android: LocalNotifications (+ native bridges later)
 ├─ Web: Service Worker + Push (kept)
 └─ Electron: OS notifications (thin adapter)

Execution modes (concise):

  • Online‑First: wake near slot → fetch (ETag, timeout) → persist → schedule; on failure → Offline‑First.
  • Offline‑First: read last good payload from SQLite; if beyond TTL → skip notification (no retry).

4) Public API (Shared by v1 & v2)

Core Types & Interface: See Implementation document for complete API definitions, type interfaces, and design decisions.

Storage semantics: 'shared' = app DB; 'private' = plugin-owned/native DB (v2). (No functional difference in v1.) Slot Identity & Scheduling PolicySlotId uses canonical HHmm and remains stable across timezone changes. • Lead window: default prefetchLeadMinutes = 20; no retries once inside the lead. • TTL policy: When offline and content is beyond TTL, we will skip the notification (no "(cached)" suffix). • Idempotency: Duplicate "scheduled" deliveries are prevented by a unique index on (slot_id, fire_at, status='scheduled'). • Time handling: Slots will follow local wall-clock time across TZ/DST; slotId=HHmm stays constant and we will recompute fire times on offset change.


5) Data Model & Retention (SQLite)

Tables: notif_contents, notif_deliveries, notif_config (see Implementation document for complete schema)

Retention: We will keep ~14 days of contents/deliveries (configurable) and will prune via a simple daily job that runs on app start/resume. We will prune daily but will not VACUUM by default on mobile; disk compaction is deferred.

Payload handling: We will template {title, body} before scheduling; we will not mutate at delivery time.


6) Scheduling Policy & Slot Math

  • One‑shot per slot per day (non‑repeating).
  • Rolling window: today's remaining slots; seed tomorrow where platform limits allow.
  • TZ/DST safe: We will recompute local wall‑times on app resume and whenever timezone/offset changes; then reschedule.
  • Android exactness: If exact alarms are unavailable or denied, we will use setWindow semantics via the scheduler adapter.
  • iOS pending cap: We will keep pending locals within typical caps (~64) by limiting the window and canceling/re‑arming as needed.
  • Electron rolling window: On Electron we will schedule the next occurrence per slot by default; depth (today+tomorrow) will be enabled only when auto-launch is on, to avoid drift while the app is closed.

7) Timing & Network Requirements

Summary: The notification system uses lightweight, ETag-aware content fetching with single attempts inside lead windows. All timing constants and detailed network policies are defined in the Implementation document.

Key Policies:

  • Lead policy: The lead window governs online-first fetch attempts, not arming. We will arm locals whenever the app runs, using the freshest available payload.
  • TTL policy: If offline and content is beyond TTL, we will skip the notification (no "cached" suffix).
  • Idempotency: Duplicate "scheduled" rows are prevented by a unique index on (slot_id, fire_at, status='scheduled').
  • Wall-clock rule: Slots will follow local wall-clock across TZ/DST; slotId=HHmm stays constant and we will recompute fire times on offset change.
  • Resume debounce: On app resume/open we will debounce pipeline entry points by 30s per app session to avoid burst fetches.
  • No scheduled background network in v1 (mobile): Local notifications will deliver offline once armed, but we will not run timed network jobs when the app is terminated. Network prefetch will occur only while the app is running (launch/resume/inside lead). Server-driven push (Web SW) and OS background schedulers are a v2 capability.

Platform-Specific Network Access:

  • iOS: Foreground/recently backgrounded only; no JS wake when app is killed
  • Android: Exact alarms vs. windowed triggers based on permissions
  • Web: Service Worker for push notifications only
  • Electron: App-running only; no background network access

Optional Background Prefetch (v1):

  • Background Runner (optional, v1): We will integrate Capacitor's Background Runner to opportunistically prefetch content on iOS/Android when the OS grants background time. This will not provide clock-precise execution and will not run after user-terminate on iOS. It will not be treated as a scheduler. We will continue to arm local notifications via our rolling window regardless of Runner availability. When Runner fires near a slot (inside prefetchLeadMinutes), it will refresh content (ETag, 12s timeout) and, behind a flag, may cancel & re-arm that slot with the fresh template if within TTL. If no budget or failure, the previously armed local will still deliver.

Implementation Details: See Implementation document for complete timing constants table, network request profiles, and platform-specific enforcement.


8) Platform Essentials

iOS

  • Local notifications will fire without background runtime once scheduled. NSE will not mutate locals; delivery-time enrichment requires remote push (future).
  • Category ID: TS_DAILY with default OPEN action
  • Background budget is short and OS‑managed; any prefetch work will complete promptly.
  • Mobile local notifications will route via action listeners (not the service worker).
  • Background Runner will offer opportunistic network wake (no guarantees; short runtime; iOS will not run after force-quit). Locals will still deliver offline once armed.

Android

  • Exact alarms on API 31+ may require SCHEDULE_EXACT_ALARM. If exact access is missing on API 31+, we will use a windowed trigger (default ±10m) and surface a settings deep-link.
  • We will deep-link users to the exact-alarm settings when we detect denials.
  • Channel defaults: ID timesafari.daily, name "TimeSafari Daily", importance=high (IDs never change)
  • Receivers for reboot/time change will be handled by v2 (plugin); in v1, re‑arming will occur on app start/resume.
  • Mobile local notifications will route via action listeners (not the service worker).
  • Background Runner will offer opportunistic network wake (no guarantees; short runtime; iOS will not run after force-quit). Locals will still deliver offline once armed.

Web

  • Requires registered Service Worker + permission; can deliver with browser closed. Web will not offline-schedule.
  • Service Worker click handlers are for web push only; mobile locals bypass the SW.
  • SW examples use /sw.js as a placeholder; wire this to your actual build output path (e.g., sw_scripts/notification-click.js or your combined bundle).
  • Note: Service workers are intentionally disabled in Electron (src/main.electron.ts) and web uses VitePWA plugin for minimal implementation.

Electron

  • We will use native OS notifications with best-effort scheduling while the app is running; true background scheduling will be addressed in v2 (native bridges).

Electron delivery strategy (v1 reality + v2 path) We will deliver desktop notifications while the Electron app is running. True background scheduling when the app is closed is out of scope for v1 and will be addressed in v2 via native bridges. We will adopt one of the following options (in order of fit to our codebase):

In-app scheduler + auto-launch (recommended now): Keep the orchestrator in the main process, start on login (tray app, hidden window), and use the Electron Notification API for delivery. This requires no new OS services and aligns with our PlatformServiceFactory/mixin patterns.

Policy (v1): If the app is not running, Electron will not deliver scheduled locals. With auto-launch enabled, we will achieve near-mobile parity while respecting OS sleep/idle behavior.

UX notes: On Windows we will set appUserModelId so toasts are attributed correctly; on macOS we will request notification permission on first use.

Prerequisites: We will require Node 18+ (global fetch) or we will polyfill via undici for content fetching in the main process.


8) Template Engine Contract

Supported tokens: {{headline}}, {{summary}}, {{date}} (YYYY-MM-DD), {{time}} (HH:MM). Escaping: HTML-escape all injected values. Limits: Title ≤ 50 chars; Body ≤ 200 chars; truncate with ellipsis. Fallback: Missing token → "[Content]". Mutation: We will render templates before scheduling; no mutation at delivery time on iOS locals.

9) Integration with Existing TimeSafari Infrastructure

Database: We will integrate with existing migration system in src/db-sql/migration.ts following the established MIGRATIONS array pattern Settings: We will extend existing Settings type in src/db/tables/settings.ts following the established type extension pattern Platform Service: We will leverage existing PlatformServiceMixin database utilities following the established mixin pattern Service Factory: We will follow the existing PlatformServiceFactory singleton pattern for notification service creation Capacitor: We will integrate with existing deep link system in src/main.capacitor.ts following the established initialization pattern Service Worker: We will extend existing service worker infrastructure following the established sw_scripts/ pattern (Note: Service workers are intentionally disabled in Electron and have minimal web implementation via VitePWA plugin) API: We will use existing error handling from src/services/api.ts following the established handleApiError pattern Logging: We will use existing logger from src/utils/logger following the established logging patterns Platform Detection: We will use existing process.env.VITE_PLATFORM patterns (web, capacitor, electron) Vue Architecture: We will follow Vue 3 + vue-facing-decorator patterns for component integration (Note: The existing useNotifications composable in src/composables/useNotifications.ts is currently stub functions with eslint-disable comments and needs implementation) State Management: We will integrate with existing settings system via PlatformServiceMixin.$saveSettings() for notification preferences (Note: TimeSafari uses PlatformServiceMixin for all state management, not Pinia stores) Identity System: We will integrate with existing did:ethr: (Ethereum-based DID) system for user context Testing: We will follow Playwright E2E testing patterns established in TimeSafari Database Architecture: We will support platform-specific database backends:

  • Web: Absurd SQL (SQLite via IndexedDB) via WebPlatformService with worker pattern
  • Capacitor: Native SQLite via CapacitorPlatformService
  • Electron: Native SQLite via ElectronPlatformService (extends CapacitorPlatformService)

10) Error Taxonomy & Telemetry

Error Codes: FETCH_TIMEOUT, ETAG_NOT_MODIFIED, SCHEDULE_DENIED, EXACT_ALARM_MISSING, STORAGE_BUSY, TEMPLATE_MISSING_TOKEN, PERMISSION_DENIED.

Event Envelope: code, slotId, whenMs, attempt, networkState, tzOffset, appState, timestamp.


11) Permission UX & Channels/Categories

  • We will request notification permission after user intent (e.g., settings screen), not on first render.
  • Android: We will create a stable channel ID (e.g., timesafari.daily) and will set importance appropriately.
  • iOS: We will register categories for optional actions; grouping may use threadIdentifier per slot/day.

12) Eventing & Telemetry

Error Codes: FETCH_TIMEOUT, ETAG_NOT_MODIFIED, SCHEDULE_DENIED, EXACT_ALARM_MISSING, STORAGE_BUSY, TEMPLATE_MISSING_TOKEN, PERMISSION_DENIED.

Event Envelope: code, slotId, whenMs, attempt, networkState, tzOffset, appState, timestamp.

Implementation: See Implementation document for complete error taxonomy, event logging envelope, ACK payload format, and telemetry events.


13) Feature Flags & Config

Key Flags: scheduler, mode, prefetchLeadMinutes, ttlSeconds, iosCategoryIdentifier, androidChannelId, prefetchRunner, runnerRearm.

Storage: Feature flags will reside in notif_config table as key-value pairs, separate from user settings.

Implementation: See Implementation document for complete feature flags table with defaults and descriptions.


14) Acceptance (Definition of Done) → Test Cases

Explicit Test Checks

  • App killed → locals fire: Configure slots at 8:00, 12:00, 18:00; kill app; verify notifications fire at each slot on iOS/Android
  • ETag 304 path: Server returns 304 → keep previous content; locals fire with cached payload
  • ETag 200 path: Server returns 200 → update content and re-arm locals with fresh payload
  • Offline + beyond TTL: When offline and content > 24h old → skip notification (no "(cached)" suffix)
  • iOS pending cap: Respect ~64 pending limit; cancel/re-arm as needed within rolling window
  • Exact-alarm denied: Android permission absent → windowed schedule (±10m) activates; UI shows fallback hint
  • Permissions disabled → we will record SCHEDULE_DENIED and refrain from queuing locals.
  • Window fallback → when exact alarm is absent on Android, verify target fires within ±10m of slot time (document as an E2E expectation).
  • Timezone change: On TZ/DST change → recompute wall-clock times; cancel & re-arm all slots
  • Lead window respect: No retries attempted once inside 20min lead window
  • Idempotency: Multiple runFullPipelineNow() calls don't create duplicate scheduled deliveries
  • Cooldown guard: deliverStoredNow() has 60s cooldown to prevent double-firing

Electron-Specific Test Checks

  • Electron running (tray or window) → notifications fire at configured slots using Electron Notification
  • Electron not running → no delivery (documented limitation for v1)
  • Start on Login enabled → after reboot + login, orchestrator will re-arm slots and deliver
  • Template limits honored (Title ≤ 50, Body ≤ 200) on Electron notifications
  • SW scope not used for Electron (click handlers are web only)
  • Windows appUserModelId set correctly for toast attribution
  • macOS notification permission requested on first use

Timing-Verifiable Test Checks

  • iOS/Android (app killed): locals will fire at their slots; no network activity at delivery time.
  • iOS/Android (resume inside lead): exactly one online-first attempt occurs; if fetch completes within 12s → content updated; otherwise offline policy applies.
  • Android (no exact access): observed delivery is within ±10 min of slot time.
  • Web push: SW push event fetch runs once with 12s timeout; if it times out, the push still displays (from payload).
  • Electron (app running): timer-based locals fire on time; on reboot with Start on Login, orchestrator re-arms on first run.
  • TTL behavior: offline & stale → skip (no notification posted).
  • ETag path: with 304, last payload remains; no duplicate scheduling rows (unique index enforced).
  • Cooldown: calling deliverStoredNow twice within 60s for same slot doesn't produce two notifications.
  • Closed app, armed earlier → locals fire at slot; title/body match last rendered content (proves "render at schedule time" + adapter API).
  • Closed app, timezone change before slot → on next resume, app recomputes and re-arms; already armed notifications will still fire on original wall-time
  • Mobile closed-app, no background network: Arm at T–hours; kill app; verify locals fire with last rendered text; confirm no network egress at delivery.
  • Web push as network scheduler: Send push with empty payload → SW fetches within 12s timeout → shows correct text; confirm behavior with browser closed.
  • Electron app not running: No delivery; with Start on Login, after reboot first run fetches and re-arms; subsequent slots fire.
  • Runner fires in background (Android/iOS): With Runner enabled and app backgrounded for ≥30 min, at least one prefetch will occur; content cache will update; already-armed locals will still fire on time.
  • Runner re-arm (flagged): If runnerRearm=true and Runner fires inside lead with fresh content + within TTL, the system will cancel & re-arm the next slot; delivered text will match fresh template.

15) Test Matrix (Essentials)

  • Android: exact vs inexact branch, Doze/App Standby behavior, reboot/time change, permission denial path, deep‑link to exact‑alarm settings.
  • iOS: BG fetch budget limits, pending cap windowing, local notification delivery with app terminated, category actions.
  • Web: SW lifecycle, push delivery with app closed, click handling, no offline scheduling.
  • Cross‑cutting: ETag/304 behavior, TTL policy, templating correctness, event queue drain, SQLite retention job.

16) Migration & Rollout Notes

  • We will keep existing web push flows intact.
  • We will introduce the orchestrator behind a feature flag, initially with a small number of slots.
  • We will migrate settings to accept multiple times per day.
  • We will document platform caveats inside user‑visible settings (e.g., Android exact alarms, iOS cap).

17) Security & Privacy

  • Tokens will reside in Keystore/Keychain (mobile) and will be injected at request time; they will not be stored in SQLite.
  • Optionally, SQLCipher at rest for mobile; redaction of PII in logs; payload size caps.
  • Content will be minimal (title/body); sensitive data will not be embedded.

18) Non‑Goals (Now)

  • Complex action sets and rich media on locals (kept minimal).
  • Delivery‑time mutation of local notifications on iOS (NSE is for remote).
  • Full analytics pipeline (future enhancement).

19) Cross-Doc Sync Hygiene

Canonical Ownership

  • This document (Plan): Canonical for Goals, Tenets, Platform behaviors, Acceptance criteria, Test cases
  • Implementation document: Canonical for API definitions, Database schemas, Adapter implementations, Code examples

PR Checklist

When changing notification system behavior, update both documents:

  • API changes: Update types/interfaces in both Plan §4 and Implementation §3

  • Schema changes: Update Plan §5 and Implementation §2

  • Slot/TTL changes: Update Plan §4 semantics and Implementation §6 logic

  • Template changes: Update Plan §9 contract and Implementation examples

  • Error codes: Update Plan §11 taxonomy and Implementation error handling

  • API code blocks: Must be identical between Plan §4 and Implementation §3 (Public API (Shared))

  • Feature flags: Must match between Plan §12 table and Implementation defaults

  • Test cases: Plan §13 acceptance criteria must align with Implementation test examples

  • Slot/TTL/Lead policies: Must be identical between Plan §4 policy and Implementation §3 policy


Sync Checklist

Sync item Plan Impl Status
Public API block identical §4 §3
getState() fields present §4 §8 Orchestrator
Capacitor action handlers §7 (iOS/Android note) §9 Bootstrap
Electron fetch prereq/polyfill §7 §9 Electron
Android ±10m fallback §7 §7 SchedulerCapacitor
Retention (no VACUUM v1) §5 $pruneNotifData
Runner described as opportunistic prefetch, not scheduler §7 §9
Feature flag prefetchRunner (default 'none') §13 §15
Capabilities `networkWake: 'opportunistic' 'none'` §7 Scheduler.capabilities
Runner tick handler bounded to ≤12s §7 BackgroundRunnerPrefetch
Optional runnerRearm flag & behavior §7 Orchestrator + Runner

Sync Checklist

Sync item Plan Impl Status
Public API block identical §4 §3
getState() fields present §4 §8 Orchestrator
Capacitor action handlers §7 (iOS/Android note) §9 Bootstrap
Electron fetch prereq/polyfill §7 §9 Electron
Android ±10m fallback §7 §7 SchedulerCapacitor
Retention (no VACUUM v1) §5 $pruneNotifData
Runner described as opportunistic prefetch, not scheduler §7 §9
Feature flag prefetchRunner (default 'none') §13 §15
Capabilities `networkWake: 'opportunistic' 'none'` §7 Scheduler.capabilities
Runner tick handler bounded to ≤12s §7 BackgroundRunnerPrefetch
Optional runnerRearm flag & behavior §7 Orchestrator + Runner

This strategic plan focuses on features and future‑tense deliverables, avoids implementation details, and preserves a clear path from the in‑app orchestrator (v1) to native plugin (v2). For executive overview, see notification-system-executive-summary.md. For complete implementation details, see notification-system-implementation.md.