mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
An account grant is one account's standing permission to read another account's record. The row is also the consent record for the act that created it: there is no second receipt table, because two records of one act drift and then nobody can say which is true. That shapes the table. Revocation stamps revoked_at and revoked_by instead of deleting, so the record can still answer who had access, from when to when, and who ended it. The uniqueness constraint is therefore partial — one live grant per (owner, delegate), with history accumulating underneath — otherwise a re-invitation would either be refused or would overwrite the first grant's acceptance. Both foreign keys cascade: a grant naming a user who no longer exists is an authorization nobody can resolve and nobody can revoke. The state machine lives in src/lib/sharing/grants.ts and only there. The acting-account resolver and the grant surfaces both read these rows, and if each carried its own idea of "active" the drift would be silent in exactly one direction: the resolver admitting a grant the panel shows as ended. Pending confers nothing, expiry is checked against the clock of the request that reads the row, and every transition is a conditional update so two concurrent accepts cannot both win. The access column carries READ or WRITE and only READ is ever written. It exists from day one so that delegated writes become enforcement work rather than migration work. Three lifecycle paths, each proven against the real route: - Delete All Data removes grants from either side. The wipe loop asks every model for "this account's rows", and a table relating two accounts has no user_id to answer with, so wipe-plan.ts now declares the owner columns and translates. Wiping only the grantor side would leave the account still holding access to other people's records. - Account deletion removes them in both directions. - An admin restore leaves them exactly as it found them. Grants are classified NOT_IN_BACKUP: rolling data back is a decision an admin makes, but rolling an authorization back is a decision nobody makes, and a grant revoked on Tuesday must not come back alive out of Monday's file. After a restore, sharing is off and both people re-consent. The backup classification guard was reading a user relation by field name, so a model naming its account relations grantor and grantee read as instance-scoped and was excused from having a verdict at all. It now reads the type. That surfaced InviteToken, classified here too.
src/
Application source. HealthLog is a Next.js 16 App Router project (output: "standalone", React 19, TypeScript strict). The path alias @/* resolves to this directory. Everything a request touches lives under here; the layout below follows the request flow rather than alphabetical order.
Request flow
client → src/proxy.ts → src/app/<route>/route.ts → src/lib/api-handler.ts → handler body → src/lib/api-response.ts → client
proxy.ts— the HTTP edge (Next.js 16's renamedmiddleware). Public-path allowlist, demo-mode mutation block, server-side onboarding redirect, worker-only refusal,x-request-id+ CSP nonce generation, every security header.app/— App Router pages (RSC by default) and API routes. Every API route wraps its handler inapiHandler. Seeapp/README.md.lib/— the server/runtime toolkit:apiHandler, auth, crypto, rollups, AI, notifications, pg-boss jobs, integrations, rate limiting. Seelib/README.md.components/— React components (shadcn/ui new-york, kebab-case files). Seecomponents/README.md.
Other directories
hooks/— shared React hooks (use-prefix, kebab-case).types/— shared TypeScript declarations.generated/prisma/— the generated Prisma client (do not edit; regenerated byprisma generate). Imported as@/generated/prisma.instrumentation.ts— Next.js startup hook; boots the pg-boss worker registry and boot-time backfill queues.__tests__/— cross-cutting unit tests; most tests live in__tests__/folders next to the code they cover.
Conventions
See ../CLAUDE.md for the full set. The load-bearing ones: RSC by default ("use client" only when a hook/state/browser API needs it); every API route wraps apiHandler and parses its body with Zod safeParse; userId is always narrowed from session or Bearer, never accepted as a body field; TanStack Query keys come only from src/lib/query-keys/; outbound HTTP goes through src/lib/safe-fetch.ts.