MalloryEstate Briefing
M·017Draft
← All briefings

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.

Dated 2026-06-13Programme Feature Waves

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.

device_tokens (D1)
Android FCM tokens, sibling to push_subscriptions; pruned on FCM UNREGISTERED/NOT_FOUND like web-push 410 pruning.
nudges (D1)
Dedup + queue + rate state: kind, dedupe_key, urgency, sent_at (null = queued) — drives dedup, the batch queue, and the daily cap.
src/fcm.ts (new)
Sends to a device token via FCM HTTP v1 using a service-account JWT; reads creds from the FCM_SERVICE_ACCOUNT secret.
src/nudge.ts + POST /nudge
Hub-pushed detected events (Access/service-token gated, same pattern as /context) run through shouldSendNow and hand off to notify().
shouldSendNow() / batchDigest() (pure)
Exhaustively unit-tested cores encoding quiet hours, dedup, urgency, daily cap, and digest coalescing — the persona-in-code.
2-hourly flush cron
Coalesces queued batch nudges into one digest ("3 messages awaiting your reply") within quiet hours, marks them sent.

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

Task-deadline and dev-backlog nudges (brief-only)A nudge-preferences UI (quiet-hours/caps stay as config constants)iOS push (no iOS shell today)Two-way notification actions (reply-from-notification)Hub job edits (mallory-runtime calendar-sync/slack-email-monitor wiring) — documented contract, separate repo
spec · docs/superpowers/specs/2026-06-13-wave-4-proactive-notifications-design.md
plan · docs/superpowers/plans/2026-06-13-wave-4-proactive-notifications.md
Mallory · Estate briefing