Feature Waves
Wave 4 — Proactive & Notifications
One unified outbound path — web push and Android FCM fanned from a single notify(), plus a conservative nudge engine.
Situation
Web push (VAPID) already worked for desktop/PWA, but the Android WebView shell had no push path at all — a raw WebView doesn't get the Push/Notifications API, so the phone needed FCM. Separately, the proactive_nudge type was reserved in webpush.ts but never wired: detected events (calendar changes, aging mentions) didn't reach Alex outbound except in the scheduled morning brief.
The guiding move is to generalise the existing notifyAll into a single notify() that fans every message to both web-push subscriptions and FCM device tokens, so every trigger — existing and new — reaches both channels with no channel-specific code. The nudge engine encodes the persona in testable, pure form: conservative by default.
Key decisions
Unified delivery
notify(env, message) fans to web-push subscriptions (existing) and FCM device tokens (new); existing callers (dig-complete, briefing, backgrounded reply) migrate transparently, no behavioural change for web.
Android FCM
A FirebaseMessagingService registers/refreshes its token to POST /push/register-token; the Worker sends via FCM HTTP v1, auth'd by a service-account JWT (mirrors existing VAPID JWT minting).
Nudge triggers (MVP)
Calendar changes and unanswered @mentions only; task-deadline and dev-backlog nudges stay brief-only for now.
Pacing constants (decided 2026-06-13, Alex)
Quiet hours 07:00–22:00 Europe/London; daily cap 10 delivered nudges; calendar now-urgency threshold = meeting within ~2h.
Urgency split
Time-critical calendar events deliver immediately within waking hours; @mentions always batch via a 2-hourly flush into one coalesced digest — never per-message.
Ships independently of Firebase
The nudge pipeline is fully functional on the existing web-push channel with no Firebase dependency, so it can land and be useful before FCM creds exist.
Architecture
A new device_tokens + nudges D1 pair, a unified notify() fan-out, and a pure nudge-decision core sitting beneath a thin Worker route + cron.
push_subscriptions; pruned on FCM UNREGISTERED/NOT_FOUND like web-push 410 pruning.FCM_SERVICE_ACCOUNT secret.shouldSendNow and hand off to notify().Risks & guardrails
Double-send
Dedup by dedupeKey plus marking sent_at is the single source of truth for what's already gone out.
Overnight burst on window open
Items queued overnight flush coalesced at the next quiet-hours window, not as a burst of separate pushes.
Channel failure isolation
A dead web subscription or FCM token is pruned without blocking the other channel or other recipients.
Blocked on external dependency
The FCM half needs Alex to create the Firebase project + service account and supply google-services.json; the nudge pipeline itself is not blocked.
Delivery
Task 1
Migration 010 — device_tokens + nudges.
Task 2
Pure nudge core — shouldSendNow + batchDigest (TDD).
Task 3
Unified notify() + device_tokens delivery + FCM stub.
Task 4
POST /push/register-token.
Task 5
POST /nudge + engine.
Task 6
2-hourly batch-flush cron.
Out of scope
docs/superpowers/specs/2026-06-13-wave-4-proactive-notifications-design.mdplan ·
docs/superpowers/plans/2026-06-13-wave-4-proactive-notifications.md