Source: ddx-documentation/research/rq7-cache-classes.md
**Question** (BRIEF.md RQ7, G6.5): which data classes use the Next.js server cache and which use TanStack Query, and how is each invalidated after a CRUD write? Evidence: one entity. **Answer:** cache by data class, not by page. Static config and org settings live in the server cache with tags. Entity lists and details live in TanStack Query with keys that start with the org. After a write, invalidate both sides: `revalidateTag` on the server and `queryClient.invalidateQueries` on the client. ## Data class table | Data class | Example | Server side | Client side | Key / tag | Invalidated by | |---|---|---|---|---|---| | Static config | brand map, nav registry, feature list, locale list | `'use cache'` + `cacheLife('max')` + `cacheTag('config')` | props from the server; no query | tag `config` | redeploy, or `revalidateTag('config')` | | Org settings | `ui.theme`, density, locale default, org branding | `'use cache'` + `cacheTag('org:<id>:settings')` in the DAL read | `SettingsProvider` unwraps the promise; TanStack Query key `['org', orgId, 'settings']` for live edits | tag `org:<id>:settings` · key `['org', orgId, 'settings']` | `PATCH .../settings/:key` → Route Handler calls `revalidateTag('org:<id>:settings')`; client calls `invalidateQueries({ queryKey: ['org', orgId, 'settings'] })` | | Session / membership | user, role, org list | never cached across requests; `cache()` per request only (RQ6) | `SessionProvider` | — | sign-in / sign-out / org switch → `router.refresh()` + `queryClient.clear()` | | Entity list | patients, appointments demo entity | first page prefetched on the server, `HydrationBoundary` | `useQuery` | key `['org', orgId, entity, 'list', params]` | create / update / delete → `invalidateQueries({ queryKey: ['org', orgId, entity] })` | | Entity detail | one record | prefetched on the server | `useQuery` | key `['org', orgId, entity, 'detail', id]` | update → `setQueryData` (optimistic) then `invalidateQueries`; delete → `removeQueries` | | Live / streamed | notifications, stream tokens | never cached | SSE manager (`lib/sse/manager.ts`) → `setQueryData` on the matching key | event `type` → key map | the SSE event itself | | Media URLs | presigned GET URL | never cached past expiry | `useQuery` with `staleTime` < URL expiry | key `['org', orgId, 'media', id, 'url']` | expiry; refetch | Key shape rule: every org-scoped key starts `['org', orgId, ...]` (`tanstack5` rule in VERSIONS.md). One `queryKeys` factory per entity builds them; no inline key arrays. ## Invalidation after a CRUD write (one entity) 1. Client calls the entity client (ADR 0006 factory, `side: 'client'`) → `/api/v1/...` BFF. 2. Optimistic variant: `onMutate` writes `setQueryData`; `onError` rolls back. Pessimistic variant: wait for the response (G8.4 asks for both). 3. On success, the client calls `invalidateQueries({ queryKey: ['org', orgId, entity] })`. 4. Server-cached data touched by the write (settings, config) is revalidated **server-side** in the Route Handler with `revalidateTag(tag)`. The client never asks the server to revalidate by name. 5. Other tabs and users get the change through SSE (event → `invalidateQueries`), not polling. ## Rules - No user data in `'use cache'` unless the org id is part of the cache key and the tag. - Session reads are never inside `'use cache'` (they read `cookies()`). - The server `QueryClient` is per request (`cache()`); the browser one is one per tab. - No `useEffect` + `fetch` for data (frontend-layout D6 review blocker). ## Sources - Next.js `cacheTag` / `revalidateTag` / `'use cache'` (Cache Components, VERSIONS.md `next16`). - TanStack Query invalidation — <https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation>. - `ddx-nextjs-frontend-layout` D3, D6, D8.
Question (BRIEF.md RQ7, G6.5): which data classes use the Next.js server cache and which use TanStack Query, and how is each invalidated after a CRUD write? Evidence: one entity.
Answer: cache by data class, not by page. Static config and org settings live in the
server cache with tags. Entity lists and details live in TanStack Query with keys that start
with the org. After a write, invalidate both sides: revalidateTag on the server and
queryClient.invalidateQueries on the client.
Key shape rule: every org-scoped key starts ['org', orgId, ...] (tanstack5 rule in
VERSIONS.md). One queryKeys factory per entity builds them; no inline key arrays.
side: 'client') → /api/v1/... BFF.onMutate writes setQueryData; onError rolls back. Pessimistic
variant: wait for the response (G8.4 asks for both).invalidateQueries({ queryKey: ['org', orgId, entity] }).revalidateTag(tag). The client never asks the server to
revalidate by name.invalidateQueries), not polling.'use cache' unless the org id is part of the cache key and the tag.'use cache' (they read cookies()).QueryClient is per request (cache()); the browser one is one per tab.useEffect + fetch for data (frontend-layout D6 review blocker).cacheTag / revalidateTag / 'use cache' (Cache Components, VERSIONS.md next16).ddx-nextjs-frontend-layout D3, D6, D8.adr/
deploy/
research/