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
Idempotency
Section titled “Idempotency”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
List members
Section titled “List members”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.
listMemberscarries 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 viaevictRevoked(...)(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.