DB Schema & Notify-Affected
This subsystem covers the relational source of truth (src/db/schema.ts) and the server-side fan-out that decides which users a write should notify (src/api/lib/notify.ts).
The notify helpers read the schema to compute recipients, then dispatch signals off the response path to each user’s inbox:
flowchart LR
ROUTES["write routes"] --> NL["notifyList()"]
ROUTES --> NU["notifyUsers()"]
ROUTES --> NM["notifyMembership()"]
NL --> AU["affectedUsers(db, listId)"]
NM --> AU
AU --> SCHEMA[("D1: lists + listMembers<br/>src/db/schema.ts")]
NL --> DISP["dispatch()"]
NU --> DISP
NM --> DISP
DISP --> ORP["offResponsePath()<br/>waitUntil"]
ORP --> FO["fanOut() — env.SYNC_INBOX"]
FO --> DO[["SyncInbox<br/>Durable Object"]]
Schema
Section titled “Schema”The Drizzle tables: users, lists, groups, tasks, steps, myDayEntries, listMembers, idempotencyKeys. See the Domain Model for columns and rules. Notable cross-cutting columns: rank (fractional ordering), assigneeId (task assignment, spec 012), and per-viewer order/group on listMembers (per-member List placement, spec 011).
Notify-affected
Section titled “Notify-affected”After a successful write, the Worker must signal the right users (ADR-011). src/api/lib/notify.ts:
affectedUsers(db, listId)— returns the owner ∪ active (non-tombstoned) members of a list: the existingaccessible()set. This is the authorization-aligned recipient set.notifyList(c, listId, surfaces)— fan a signal to everyone with access to a list, for the given surface tags.notifyUsers(c, userIds, surfaces)— signal a specific set of users (e.g. the acting user for owner-scoped surfaces like My Day).notifyMembership(c, …)— handle membership add/remove, which must also notify the user whose access just changed.
The sequence below traces a list-scoped write: the handler returns immediately while dispatch resolves recipients and RPCs each inbox under waitUntil.
sequenceDiagram participant Route as "write route" participant Notify as "notifyList()" participant Dispatch as "dispatch() / offResponsePath()" participant AU as "affectedUsers()" participant D1 as "D1: lists + listMembers" participant DO as "SyncInbox DO" Route->>Notify: notifyList(c, listId, surfaces) Notify->>Dispatch: build signal, waitUntil(...) Route-->>Route: respond to client (committed write) Dispatch->>AU: resolve recipients AU->>D1: owner ∪ active members D1-->>AU: userIds Dispatch->>DO: fanOut() — notify(signal) per user Note over Dispatch,DO: failures logged, never fail the write (F-17/F-18)
Fan-out runs off the request path and is bounded by member count; a notify failure never fails the already-committed write (constitution F-17/F-18). The internal dispatch(...) helper sends via offResponsePath(c, …), which calls c.executionCtx.waitUntil(...) in the Worker runtime and falls back to a detached run under a test harness that has no ExecutionContext — so a notify never breaks the handler. The recipient set deliberately mirrors the read-authorization rule, so a user is only ever signalled about data they can actually read.
Related: Worker API & Clerk Auth (the write routes), SyncInbox Durable Object (the delivery target).