Skip to content

feat(reminders): server-side meal reminders — Cron → first Edge Function → Pushover (007) - #27

Merged
rghamilton3 merged 1 commit into
mainfrom
feat/007-meal-reminders
Jul 26, 2026
Merged

rghamilton3 merged 1 commit into
mainfrom
feat/007-meal-reminders

Conversation

@rghamilton3

Copy link
Copy Markdown
Contributor

Closes #15.

Feature 007 — the last unbuilt P1 item. Server-side meal reminders: pg_cron → the project's first Edge Function → Pushover, plus the "Gentle nudges" setup screen from design/screens/mobile-reminder-setup.html. Covers SRS §3.6.1 (REQ-MR-001..005) and REQ-UX-010; enforces INV-PL-010/011.

Database — 0010_meal_reminders.sql

  • meal_reminders — one per (owner, slot): local reminder_time, optional pre_alert_minutes (REQ-MR-002), ISO days_of_week (trigger-normalized; INV-PL-011 as CHECK), notification_type (PUSH/SILENT/NONE), and the owner's IANA timezone. The domain docs store a LocalTime while Cron runs UTC — the zone column (captured from the device at save time) closes that gap, with all conversion done by Postgres.
  • reminder_deliveries — occurrence-unique dedup ledger (UNIQUE (reminder_id, kind, local_date)). Written only by service_role: authenticated/anon get no write grants and no write policy, so no SECURITY DEFINER ownership guard is needed — deliberately avoiding the SECURITY DEFINER ownership guards block service-role inserts that carry references (auth.uid() is NULL) #14 pattern that blocks service-role inserts (auth.uid() NULL). Owners can SELECT their own history. Failures stay visible as FAILED rows; nothing is retried or delivered late (REQ-MR-005).
  • due_meal_reminders() — every timezone/day-of-week/window/dedup decision lives in SQL where pgTAP proves it; the RPC also joins the day's still-PLANNED meal for personalized copy. EXECUTE revoked from anon/authenticated.
  • Cron wiring — pg_cron every minute → invoke_send_meal_reminders() → pg_net POST. No-ops until the Vault secrets exist, so fresh/local environments migrate cleanly and CI is unaffected.

Edge Function — send-meal-reminders (bootstraps supabase/functions/)

Thin dispatch loop: due occurrences → claim via the dedup key (upsert … ignoreDuplicates, safe under overlapping runs) → Pushover → mark SENT/FAILED. Copy is guilt-free per design/screens/mobile-reminder.html ("No rush — just a gentle nudge.").

Caller gate (from supabase-rls-reviewer feedback): verify_jwt is off — the anon key is a valid, public JWT, so a JWT check gates nothing. Instead the function requires an x-cron-secret shared secret (Vault ↔ function env), compared constant-time, fail-closed (401 without it). Verified locally: no header → 401, wrong secret → 401, anon JWT → 401, correct secret → 200.

Setup (per environment) is documented in supabase/functions/README.md: CRON_SHARED_SECRET + Pushover token/user key as function secrets, project_url + send_meal_reminders_secret in Vault.

Client

  • /reminders — the Gentle nudges screen: per-slot time input, pre-alert select, toggle; batch upsert on (owner, slot) stamped with the device time zone. Svelte 5 runes, design tokens, feature-folder anatomy matching src/lib/log/.
  • Entry points: desktop sidebar Nudges; a bell in the Today header (mobile tabs stay at five).

Verification

  • pgTAP ×31 (reminders_rls.test.sql): RLS isolation both directions, schema invariants (INV-PL-010/011, tz validity, pre-alert bounds, slot uniqueness, day normalization), client write-denial on the ledger, server-only RPC, timezone math (New York + Tokyo), window edges (never late), day-of-week, suppression (disabled/SILENT), dedup, cron job existence, invoker no-op.
  • Unit ×6 (types.spec.ts) — mapping/merge/defaults. e2e ×4 (reminders.e2e.ts) — defaults render, save persists all slots + timezone, edits round-trip a reload, bell/Maybe-later navigation.
  • Full suites green: 131 pgTAP / 275 unit / 29 e2e; check, lint, build clean.
  • Manually driven end-to-end against the local stack: real due reminder → function claims → Pushover call (fake creds → FAILED row with the API response captured) → second invoke returns due: 0.

Notes / follow-ups

  • SECURITY DEFINER ownership guards block service-role inserts that carry references (auth.uid() is NULL) #14 stays open: this feature avoids the guard pattern rather than resolving it; the decision for the existing guards is still to be made.
  • Day-of-week picking has schema + RLS support but no UI yet (defaults to every day).
  • Pushover recipient is a single PUSHOVER_USER_KEY env — fine while auth is anonymous-only; a per-user key column is the obvious extension when real auth lands.
  • The client nudge banner (SLOT_WINDOWS in src/lib/log/slots.ts) still uses its fixed windows; personalizing it from meal_reminders is a natural follow-up.

…ion → Pushover (007)

Closes #15. Covers SRS §3.6.1 (REQ-MR-001..005) and REQ-UX-010; enforces
INV-PL-010/011.

Database (0010, pgTAP ×31):
- meal_reminders: one per (owner, slot) — time, optional pre-alert, ISO
  days-of-week (normalized by trigger), notification type, and the owner's
  IANA time zone (resolves the LocalTime-vs-UTC-cron gap in the domain docs)
- reminder_deliveries: occurrence-unique dedup ledger, written only by
  service_role (no client write grants — deliberately sidesteps the #14
  SECURITY DEFINER guard pattern; owners can SELECT their history)
- due_meal_reminders(): all timezone/window/day/dedup math in SQL where
  pgTAP proves it; joins the day's still-PLANNED meal for personalized copy;
  EXECUTE revoked from anon/authenticated
- pg_cron every minute → invoke_send_meal_reminders() → pg_net POST;
  no-ops until Vault is configured, so every environment migrates cleanly

Edge Function (the project's first — bootstraps supabase/functions/):
- send-meal-reminders: claim occurrence via the dedup key → Pushover →
  record SENT/FAILED; missed nudges are dropped, never delivered late
- gated by a fail-closed x-cron-secret shared secret compared constant-time
  (verify_jwt off: the anon key is a public, valid JWT and gates nothing)
- guilt-free copy per design/screens/mobile-reminder.html

Client (design/screens/mobile-reminder-setup.html):
- /reminders "Gentle nudges" screen — per-slot time, pre-alert, and toggle;
  batch upsert stamped with the device time zone
- entry points: desktop sidebar "Nudges", Today-header bell (mobile)
- src/lib/reminders/ service/store/types per the established feature anatomy

Tests: 31 pgTAP + 6 unit + 4 e2e, all suites green (275 unit / 29 e2e total).
@rghamilton3
rghamilton3 merged commit 25dabfd into main Jul 26, 2026
4 checks passed
@rghamilton3
rghamilton3 deleted the feat/007-meal-reminders branch July 26, 2026 19:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature 007: Meal reminders (Supabase Cron → Pushover, first Edge Function)

1 participant