# Storage adapters
Storage is behind the NotificationStore interface
(packages/api/src/store/types.ts).
Two adapters ship today:
| Driver | Use |
|---|---|
supabase |
Production — raw PostgREST over fetch with the service role key |
memory |
Tests and local dev (DB_DRIVER=memory) |
# 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.