# 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, …)

  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.