# 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.varswhen exploring the API. - Perfect for: Cognito or API-key auth dry runs, OpenAPI
/docs, client integration againsthttp://127.0.0.1:8787. - After you publish a notification, you can list it only while that
wrangler devprocess keeps running. Restart → empty store → republish. - Dedupe works inside that process; two parallel local Workers would not share state.
GET /healthalways succeeds (pingis a no-op).
# Supabase in practice
- Production / staging path: Postgres table + indexes from the migration; Worker uses PostgREST over
fetchwith 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=supabasein.dev.varswith 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, …)
- Implement
NotificationStoreinpackages/api/src/store/<name>.ts. The contract:- Every method is user-scoped.
list,markRead,markAllRead,delete,unreadCountmust filter byuserGuid. This is the service’s ownership enforcement — there is no second check in route handlers. createmust be race-safe idempotent on(userGuid, source, dedupeKey)whendedupeKeyis non-null: enforce it with a unique constraint at the database, not check-then-insert. Return the existing row withduplicate: trueon conflict.listorders bycreatedAt DESC, id DESCwith keyset (not offset) pagination, returningnextCursoronly when more rows exist. Timestamps are ISO-8601 strings.markReadreturnstruefor “exists and is yours” even if already read (idempotent);readAtis set only on the first transition.purgeOlderThan(days)supports the retention cron.
- Every method is user-scoped.
- Register the driver in
packages/api/src/store/index.tsand extend theDB_DRIVERenum + any new env vars insrc/config.ts. - Verify behaviour matches the reference: copy the assertions in
test/memory-store.test.tsand run them against your adapter (integration or mocked transport, liketest/supabase-store.test.ts). - 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.