Skip to content

Idempotency & List-Members Repositories

Two repositories underpin reliable sync and sharing: idempotent replay and list membership.

Each repository is a factory over a D1 table, consumed by middleware and routes respectively:

flowchart LR
  MW["idempotency middleware<br/>src/api/middleware/idempotency.ts"] --> IR["createIdempotencyRepository(db)<br/>src/db/repositories/idempotency.ts"]
  IR --> IK[("D1: idempotencyKeys")]
  LMR_ROUTE["registerListMembersRoutes<br/>src/api/routes/list-members.ts"] --> LMR["createListMembersRepository(db)<br/>src/db/repositories/list-members.ts"]
  LMR --> LM[("D1: listMembers")]
  LMR_ROUTE --> NM["notifyMembership()<br/>src/api/lib/notify.ts"]
  OUTBOX["client outbox replay"] -->|"Idempotency-Key"| MW

createIdempotencyRepository(db) (src/db/repositories/idempotency.ts) backs the idempotencyKeys table. Each queued client mutation carries a stable Idempotency-Key; on replay the server records/looks up the key so a re-sent mutation does not double-apply. This is what makes outbox replay safe after offline periods or retries (constitution §7.14.3; see Offline Outbox & Optimistic Writes).

On a replayed mutation the middleware lookups the key and short-circuits with the stored response; on a fresh one it runs the handler and stores the first 2xx:

sequenceDiagram
  participant Client
  participant MW as "idempotency middleware"
  participant Repo as "idempotencyRepository"
  participant Handler as "route handler"
  participant D1 as "D1: idempotencyKeys"
  Client->>MW: mutation + Idempotency-Key
  MW->>Repo: lookup(key)
  Repo->>D1: select by key
  alt key exists (replay)
    D1-->>Repo: stored record
    MW-->>Client: replay stored response (no re-apply)
  else fresh key
    D1-->>Repo: none
    MW->>Handler: next()
    Handler-->>MW: 2xx response
    MW->>Repo: store(key, ownerId, fp, response)
    Repo->>D1: insert record
    MW-->>Client: response
  end

createListMembersRepository(db) (src/db/repositories/list-members.ts) backs the listMembers table — the join between Lists and the Users they’re shared with (Members). Domain rules (Product Brief §5.2):

  • Only the List owner changes memberships (add/remove Members); a List can only be shared with registered Users.
  • listMembers carries per-viewer order + group for shared Lists, so each Member arranges a shared List in their own sidebar independently of the owner (spec 011).
  • Membership changes drive notifyMembership(...), which signals both the list’s users and the user whose access changed; revoked access is evicted client-side via evictRevoked(...) (see DB Schema & Notify-Affected, Sync-Core Reconcilers).
  • Assignment (spec 012) depends on membership: a Task may be assigned only to a Member; removing a Member clears their assignments.

Related: DB Schema & Notify-Affected, Offline Outbox & Optimistic Writes, Worker API & Clerk Auth.