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 Policy • SlotId uses canonical
HHmm
and remains stable across timezone changes. • Lead window: defaultprefetchLeadMinutes = 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 defaultOPEN
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 timeoutETAG_NOT_MODIFIED
- Server returned 304 (expected)SCHEDULE_DENIED
- OS denied notification schedulingEXACT_ALARM_MISSING
- Android exact alarm permission absentSTORAGE_BUSY
- Database locked or unavailableTEMPLATE_MISSING_TOKEN
- Required template variable not foundPERMISSION_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
fromsrc/services/api.ts
- Logging: Uses existing logger from
src/utils/logger
with established patterns - Platform Detection: Uses existing
Capacitor.isNativePlatform()
andVITE_PLATFORM
patterns
File Organization Compliance
- Services: Follows existing
src/services/
organization with factory and adapters - Database: Extends existing
src/db-sql/migration.ts
andsrc/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
.