The MEDICATION_INTAKE_SYNC fan-out used to fire one silent APNs per distinct dose slot, so a bulk intake marking five doses woke every other device five times. Replace the per-slot dispatch with a per-user coalescer: the first mutation in a burst dispatches immediately, every further mutation inside the 2 s window folds into one trailing fan-out. A bulk request now costs exactly one queue call, and a rapid burst of N single-dose mutations costs at most two fan-outs. The window state is in-process; multiple app instances throttle independently, which is accepted because the iOS reconcile is idempotent and the deployment model is a single container. The silent payload shrinks to a pure sync trigger — eventType + syncedAt. The per-slot medicationId / scheduledFor fields are gone: the iOS handler runs a full reconcile regardless, a coalesced burst has no single slot to name, and a push payload travels through Apple's infrastructure, so it now carries no health data by construction. The Live Activity end push likewise sends only the ActivityKit envelope. The trailing flush keeps the origin skip only when every coalesced mutation came from the same device; a mixed-origin burst wakes everyone. Each flush still records one push_attempts ledger row, reaps hard-rejected device tokens, and annotates the wide event with the recipient count and coalesced-mutation count.
src/app/
The Next.js App Router tree: the page surfaces a logged-in user sees and the REST API under api/. Server components are the default; "use client" is added only where a hook, state, or browser API needs it. Routing is file-based — a page.tsx is a page, a route.ts is an API endpoint.
API routes (api/)
Every endpoint wraps its handler in apiHandler (@/lib/api-handler), parses any body with Zod safeParse, narrows userId from the session or Bearer token (never from the body), and returns the { data, error, meta? } envelope. requireAdmin() is cookie-only, so api/admin/ is unreachable by Bearer token. Notable groups:
auth/— login, register, password, passkey (WebAuthn), refresh, Codex device-OAuth.measurements/,mood-entries/,medications/,sleep/— the core health-data CRUD + batch/bulk ingest.ingest/— external Bearer-token ingest surfaces.insights/,analytics/,dashboard/— AI insights, the Coach SSE stream, trend summaries, the first-paint dashboard snapshot.withings/,whoop/,fitbit/,integrations/,sync/— OAuth connect/sync/webhook surfaces and the offline delta feed.fhir/,share-links/,doctor-report/,export/,import/— clinician-facing FHIR R4 read API, shareable records, report + data export.notifications/,devices/,send/,telegram/— channel registration, device tokens, dispatch, webhook intake.admin/— operator panel APIs (cookie-only).health/,version/,meta/,internal/— health check, build-version probe, runtime metadata, worker-internal routes.
Page surfaces
measurements/, medications/, mood/, insights/, achievements/, notifications/, settings/, onboarding/, admin/, plus the public about/, privacy/, bugreport/, the share-record route c/, and the root dashboard (page.tsx). Shared shell + chrome lives in layout.tsx; error.tsx / global-error.tsx / not-found.tsx / loading.tsx cover the framework boundaries. layout.tsx holds the one nonce-bound dangerouslySetInnerHTML (theme bootstrap).
Conventions
See ../../CLAUDE.md. The request-flow walk in .planning/codebase/arch.md annotates each layer with file:line citations. Related dirs: handlers compose ../lib/; pages render ../components/.