# Storage backends

Storage is behind the NotificationStore interface (packages/api/src/store/types.ts). Choose the driver with DB_DRIVER:

Driver Env Typical use
memory DB_DRIVER=memory Local wrangler dev, unit tests
supabase DB_DRIVER=supabase + SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY Staging and production

Both implement the same API contract: publish, list, unread count, mark read/unread, mark all read, delete, retention purge, and health ping. Auth (Cognito / API keys) does not change between drivers.


# Memory vs Supabase — what you get

Capability Memory Supabase
Full V2 + V1 HTTP API Yes Yes
Dedupe (userGuid + source + dedupeKey) Yes (in-process) Yes (partial unique index + conflict ignore — race-safe across instances)
Keyset pagination, unread filters, since Yes Yes
Retention cron (purgeOlderThan) Yes (while the process is alive) Yes (persisted; cron runs on the deployed Worker)
Data survives Worker restart No — wiped when wrangler dev / the isolate stops Yes
Shared across machines / deploys No — one process’s RAM only Yes — all Worker isolates talk to the same Postgres
Needs a database project No Yes — apply 0001_notifications.sql
Needs SUPABASE_* secrets No Yes (service role; never commit)
RLS deny-all / service-role-only writes N/A Yes
Suitable for production traffic No Yes
Suitable for “publish then list” dry runs Yes (same session) Yes

For how multi-isolate Workers + one DB relate to resilience (and future failover ideas), see High availability.

# Memory in practice

  • Default for local .dev.vars when exploring the API.
  • Perfect for: Cognito or API-key auth dry runs, OpenAPI /docs, client integration against http://127.0.0.1:8787.
  • After you publish a notification, you can list it only while that wrangler dev process keeps running. Restart → empty store → republish.
  • Dedupe works inside that process; two parallel local Workers would not share state.
  • GET /health always succeeds (ping is a no-op).

# Supabase in practice

  • Production / staging path: Postgres table + indexes from the migration; Worker uses PostgREST over fetch with the service role (not the anon key, not end-user JWTs on the DB).
  • Data persists across deploys and restarts; multiple Worker isolates share one database.
  • Dedupe is enforced at the database (safe under concurrent publishes).
  • Retention cron actually keeps storage bounded over time.
  • Local option: set DB_DRIVER=supabase in .dev.vars with a real project URL + service role if you want persistence while still developing on your laptop.

# How to switch

# Local throwaway (default for quick starts)
DB_DRIVER=memory

# Local or cloud with real persistence
DB_DRIVER=supabase
SUPABASE_URL=https://YOUR_PROJECT.supabase.co
SUPABASE_SERVICE_ROLE_KEY=...   # dashboard Secret / wrangler secret / .dev.vars only

Restart wrangler dev after changing .dev.vars. Deployed Workers set DB_DRIVER=supabase (and Supabase URL/key) in the Cloudflare dashboard — not in wrangler.toml.


# Adding a new backend (D1, Hyperdrive/Postgres, Turso, …)

  1. Implement NotificationStore in packages/api/src/store/<name>.ts. The contract:
    • Every method is user-scoped. list, markRead, markAllRead, delete, unreadCount must filter by userGuid. This is the service’s ownership enforcement — there is no second check in route handlers.
    • create must be race-safe idempotent on (userGuid, source, dedupeKey) when dedupeKey is non-null: enforce it with a unique constraint at the database, not check-then-insert. Return the existing row with duplicate: true on conflict.
    • list orders by createdAt DESC, id DESC with keyset (not offset) pagination, returning nextCursor only when more rows exist. Timestamps are ISO-8601 strings.
    • markRead returns true for “exists and is yours” even if already read (idempotent); readAt is set only on the first transition.
    • purgeOlderThan(days) supports the retention cron.
  2. Register the driver in packages/api/src/store/index.ts and extend the DB_DRIVER enum + any new env vars in src/config.ts.
  3. Verify behaviour matches the reference: copy the assertions in test/memory-store.test.ts and run them against your adapter (integration or mocked transport, like test/supabase-store.test.ts).
  4. Schema: mirror supabase/migrations/0001_notifications.sql — same columns, the three indexes, and the partial unique dedupe index.

Keep DB naming (snake_case) inside the adapter; the rest of the codebase only sees the camelCase Notification type.