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.
 
 
 
 
 
 

26 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

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)

export type NotificationTime = { hour: number; minute: number }; // local wall-clock
export type SlotId = string; // Format: "HHmm" (e.g., "0800", "1200", "1800") - stable across TZ changes

export type FetchSpec = {
  method: 'GET'|'POST';
  url: string;
  headers?: Record<string,string>;
  bodyJson?: Record<string,unknown>;
  timeoutMs?: number;
};

export type CallbackProfile = {
  fetchContent: FetchSpec;
  ackDelivery?: Omit<FetchSpec,'bodyJson'|'timeoutMs'>;
  reportError?: Omit<FetchSpec,'bodyJson'|'timeoutMs'>;
  heartbeat?: Omit<FetchSpec,'bodyJson'> & { intervalMinutes?: number };
};

export type ConfigureOptions = {
  times: NotificationTime[];                  // 1..M daily
  timezone?: string;                          // Default: system timezone
  ttlSeconds?: number;                        // Default: 86400 (24h)
  prefetchLeadMinutes?: number;               // Default: 20
  storage: 'shared'|'private';               // Required
  contentTemplate: { title: string; body: string }; // Required
  callbackProfile?: CallbackProfile;          // Optional
};

export interface MultiDailyNotification {
  requestPermissions(): Promise<void>;
  configure(o: ConfigureOptions): Promise<void>;
  runFullPipelineNow(): Promise<void>;        // API→DB→Schedule (today's remaining)
  deliverStoredNow(slotId?: SlotId): Promise<void>; // 60s cooldown guard
  reschedule(): Promise<void>;
  getState(): Promise<{
    nextOccurrences: Array<{ slotId: SlotId; when: string }>; // ISO
    lastFetchAt?: string; lastDeliveryAt?: string;
    pendingCount: number; exactAlarmCapable?: boolean;
  }>;
}

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

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. v1 will schedule the next occurrence per slot by default; a configurable depth (0=today, 1=today+tomorrow) may be enabled as long as the iOS pending cap is respected.
  • 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) 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).

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).

Web

  • Requires registered Service Worker + permission; can deliver with browser closed. Web will not offline-schedule.
  • Service worker click handlers apply to web push only; local notifications on mobile do not pass through 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 Taxonomy

Finite error code set:

  • FETCH_TIMEOUT - Network request exceeded timeout
  • ETAG_NOT_MODIFIED - Server returned 304 (expected)
  • SCHEDULE_DENIED - OS denied notification scheduling
  • EXACT_ALARM_MISSING - Android exact alarm permission absent
  • STORAGE_BUSY - Database locked or unavailable
  • TEMPLATE_MISSING_TOKEN - Required template variable not found
  • PERMISSION_DENIED - User denied notification permissions

Event Logging Envelope

{
  code: string,           // Error code from taxonomy
  slotId: string,         // Affected slot
  whenMs: number,         // Scheduled time
  attempt: number,        // Retry attempt (1-based)
  networkState: string,   // 'online' | 'offline'
  tzOffset: number,       // Current timezone offset
  appState: string,       // 'foreground' | 'background' | 'killed'
  timestamp: number       // UTC timestamp
}

ACK Payload Format

{
  slotId: string,
  fireAt: number,         // Scheduled time
  deliveredAt: number,    // Actual delivery time
  deviceTz: string,       // Device timezone
  appVersion: string,     // App version
  buildId: string         // Build identifier
}
  • Event queue (v1): In-memory queue for delivery, error, heartbeat events. Background/native work will enqueue; foreground will drain and publish to the UI. v2 will migrate to SQLite-backed queue for persistence.
  • Callbacks (optional): ackDelivery, reportError, heartbeat will post to server endpoints when configured.
  • Minimal metrics: pending count, last fetch, last delivery, next occurrences.

13) Feature Flags & Config

Feature Flags Table

Flag Default Description Location
scheduler 'capacitor' Scheduler implementation notif_config table
mode 'auto' Online-first inside lead, else offline-first notif_config table
prefetchLeadMinutes 20 Lead time for prefetch attempts notif_config table
ttlSeconds 86400 Content staleness threshold (24h) notif_config table
iosCategoryIdentifier 'TS_DAILY' iOS notification category notif_config table
androidChannelId 'timesafari.daily' Android channel ID (never changes) notif_config table

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


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

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

Synchronization Points

  • 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

21) Privacy & Security Alignment

Privacy-First Architecture

  • User-Controlled Visibility: Notification preferences will be user-controlled with explicit opt-in/opt-out
  • Data Sovereignty: All notification data will reside on user's device; no external tracking or analytics
  • Minimal Data Collection: We will collect only essential data for notification delivery (slot times, content templates)
  • DID Integration: Notifications will be associated with user's Decentralized Identifier (DID) for privacy-preserving identity

Security Considerations

  • Content Encryption: Sensitive notification content will be encrypted at rest using device keystore
  • Secure Transmission: All API calls will use HTTPS with proper certificate validation
  • Input Validation: All notification content will be validated and sanitized before storage
  • Access Control: Notification settings will be protected by user authentication

Compliance with TimeSafari Principles

  • Privacy-Preserving: Follows TimeSafari's privacy-preserving claims architecture
  • User Agency: Users maintain full control over their notification experience
  • Transparency: Clear communication about what data is collected and how it's used
  • Minimal Footprint: Notification system will have minimal impact on user privacy

23) Platform-Specific Implementation Details

Web Platform (VITE_PLATFORM=web)

  • Database: Uses Absurd SQL (SQLite via IndexedDB) via WebPlatformService with worker pattern
  • Notifications: Web push notifications via Service Worker (minimal implementation)
  • Local Scheduling: Not supported - web cannot schedule local notifications offline
  • API Integration: Direct HTTP calls for content fetching
  • Storage: Notification preferences stored in Absurd SQL database
  • Testing: Playwright E2E tests run on web platform

Capacitor Platform (VITE_PLATFORM=capacitor)

  • Database: Uses native SQLite via CapacitorPlatformService
  • Notifications: Local notifications via @capacitor/local-notifications
  • Local Scheduling: Fully supported - OS-level notification scheduling
  • API Integration: HTTP calls with mobile-optimized timeouts and retry logic
  • Storage: Notification preferences stored in native SQLite database
  • Testing: Playwright E2E tests run on mobile devices (Android/iOS)

Electron Platform (VITE_PLATFORM=electron)

  • Database: Uses native SQLite via ElectronPlatformService (extends CapacitorPlatformService)
  • Notifications: OS-level notifications via Electron's notification API
  • Local Scheduling: Supported - desktop OS notification scheduling
  • API Integration: Same as Capacitor platform
  • Storage: Same as Capacitor platform (via inherited service)
  • Testing: Same as Capacitor platform

Cross-Platform Considerations

  • Feature Detection: Use process.env.VITE_PLATFORM for platform-specific behavior
  • Database Abstraction: PlatformServiceMixin handles database differences transparently
  • API Consistency: Same TypeScript API across all platforms
  • Fallback Behavior: Web platform gracefully degrades to push-only notifications

24) TimeSafari Architecture Compliance

Design Pattern Adherence

  • Factory Pattern: Notification service follows PlatformServiceFactory singleton pattern
  • Mixin Pattern: Database access uses existing PlatformServiceMixin pattern
  • Migration Pattern: Database changes follow existing MIGRATIONS array pattern
  • Error Handling: Uses existing handleApiError from src/services/api.ts
  • Logging: Uses existing logger from src/utils/logger with established patterns
  • Platform Detection: Uses existing Capacitor.isNativePlatform() and VITE_PLATFORM patterns

File Organization Compliance

  • Services: Follows existing src/services/ organization with factory and adapters
  • Database: Extends existing src/db-sql/migration.ts and src/db/tables/settings.ts
  • Utils: Extends existing src/utils/PlatformServiceMixin.ts
  • Main Entry: Integrates with existing src/main.capacitor.ts initialization
  • Service Workers: Follows existing sw_scripts/ organization

Type Safety Compliance

  • Settings Extension: Follows existing Settings type extension pattern
  • Interface Definitions: Uses existing TypeScript interface patterns
  • Error Types: Follows existing error handling type patterns
  • Platform Types: Uses existing platform detection type patterns

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

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