Domain data is not duplicated into the query cache. A client view keeps what it received in local state and updates it from action results.
The QueryClient
modules/shared/lib/query-client.ts:
staleTimeis one minute.retryis off. A failed request surfaces at once.- Pending queries are dehydrated too, so a prefetch that has not finished can stream.
Hydration
Session and organization list
(authenticated)/layout.tsx prefetches on the server and dehydrates:
useSessionQuery() reads the same key with staleTime: Infinity, refetchOnWindowFocus: false and retry: false. The session is therefore fetched once per full page load and then updated by hand:
reloadSession()fromuseSession()refetches withdisableCookieCacheand writes the result withsetQueryData.LoginForminvalidatessessionQueryKeyafter a successful sign in.setActiveOrganization()patchesactiveOrganizationIdandlastActiveOrganizationIdin place.
The active organization boundary
[organizationSlug]/layout.tsx prefetches the organization and wraps its subtree in a second HydrationBoundary:
dehydrate() snapshot is taken before the child layout has run its prefetch. Without a boundary of its own, the organization would be missing from the client cache on the first paint.
That matters because NavBar gates every menu item except the home item on activeOrganization. With an empty cache the sidebar showed only the first item and filled in the rest after a client refetch. With the boundary the full menu is in the server HTML.
The general rule: with a request scoped server QueryClient, a parent layout’s dehydrated state never includes prefetches made by a child layout. Any layout or page that prefetches must render its own
HydrationBoundary. admin/organizations/[id]/page.tsx does.useActiveOrganizationQuery sets placeholderData: (previous) => previous, so switching between two studios keeps the old one on screen until the new one arrives. ActiveOrganizationProvider treats placeholder data as not yet loaded when computing loaded.
Query keys
Hand written keys
Scope every domain key by
slug. A user can belong to several studios and the cache outlives navigation between them.
The food library key is shared on purpose. Editing a food in the foods page, changing MBP anchors in settings and editing inside the nutrition builder all call invalidateQueries({ queryKey: ["food-library", slug], refetchType: "all" }), so a stale copy never resurfaces in another screen.
oRPC keys
modules/shared/lib/orpc-query-utils.ts exports orpc, built with createTanstackQueryUtils(orpcClient). Each procedure gets queryOptions, mutationOptions and queryKey helpers:
The router type is
modules/shared/lib/orpc-router.generated.d.ts. It is excluded from oxlint in .oxlintrc.json. No script in the frontend repo generates it, and a comment in modules/admin/lib/admin-api.ts says it is kept by hand and lags new procedures. When the backend adds a procedure, update this file before calling it.
orpcClient throws if used on the server (RPCLink is not allowed on the server side). On the server use createServerOrpcClient() from bff-server-fetch.ts. The client logs errors through logError, except aborted requests and SERVICE_UNAVAILABLE.
Server side request caching
Reactcache() deduplicates within one request:
- All helpers in
modules/auth/lib/server.ts(getSession,getActiveOrganization,getOrganizationListand the rest). listPurchasesinmodules/payments/lib/server.ts.loadSigninginmodules/sign/sign-upstream.ts.
getSession() and only one request reaches the API. resolveStudioContext is not wrapped itself, but the two helpers it calls are.
performApi calls are not deduplicated. getStudioAppearance(slug) is called by the organization layout and again by pages that need the brand colour, and each call reads /studios/current.
No fetch in the app opts into the Next.js data cache. fetchAuthApi sets cache: "no-store" and the signed client uses default fetch behaviour inside dynamic routes.
Pagination and lists
Pickers that search trainees call a server action per keystroke (for example
searchTrainees in the dashboard actions, pageSize: 6). They do not take a snapshot of the whole trainee list.
URL state
nuqs is mounted through NuqsAdapter in the root layout. SocialSigninButton reads invitationId with useQueryState. The trainee board stores its filter rules in ?rules= as JSON and its selection in ?client=. The plan lists use ?scope=training or ?scope=nutrition, and ?ai=1 opens the import wizard.