Performance patterns
Keep Concourse pages fast, responsive, and safe to cache
Concourse uses Next.js Cache Components and partial prefetching. New code must make a clear choice about freshness, streaming, and feedback before it adds a read or action.
Choose a cache boundary
Cache the smallest function that owns stable data. Keep authentication and access checks outside the cached function. Organization membership, SSO, and administration reads remain request-bound.
Use use cache for data that is safe to share between users, such as processed documentation.
Pass serializable arguments and set cacheLife in the same function. Add a tag when a write can make the result stale.
After a successful mutation, call updateTag for the changed data. Call revalidatePath when the
rendered route also needs to refresh.
Use use cache: private only when a request API must be read inside a cache boundary and the value
must stay in browser memory. It is suitable for the docs sidebar cookie. It is not a server cache for
user data.
Use use cache: remote only for an expensive, shared, low-cardinality operation where the team has
approved remote cache storage, cost, and invalidation. A remote cache is not a default replacement
for the in-memory cache.
Authentication, authorization, membership, SSO, account security, and audit reads stay fresh and
request-bound. Do not place a session, headers(), or access decision inside a shared use cache
scope.
See the Next.js use cache reference,
cacheLife, and
cacheTag for the framework rules.
Use the Better Auth session cache
Better Auth uses a separate signed cookie cache for short-lived session data. Concourse keeps the
compact cache enabled for five minutes, as configured in packages/auth/lib/identity/configuration.ts. This reduces repeated session database reads for the application shell's readable
navigation display.
The cookie cache is not a content cache. Use Next.js use cache for shareable documentation
content, with an explicit lifetime and invalidation tag. Keep session and access decisions outside
that content cache.
For a static documentation site, keep the shared layout free of cookies(), headers(), and other
request-time reads. A request-time read in a layout can block the route before its page fallback is
shown and can prevent full route prefetching. Store browser-only preferences in client state or an
isolated boundary when the preference is not part of the page content. Do not add a route
loading.tsx to generated content unless the route becomes dynamic. A loading boundary changes the
prefetch shape, and build-time content does not need one.
When partialPrefetching is enabled, use prefetch={true} for links to static pages whose content
depends on params or searchParams. The default partial prefetch only prepares the shared shell;
the explicit value resolves the URL-specific page before the click. Use this for a small static docs
navigation, not for large lists of links or expensive dynamic pages.
Use the cookie cache for:
- application navigation chrome
- readable session display that does not decide access
Use disableCookieCache: true for:
- account security
- user management
- organization membership and SSO management
- system administration
- every authorization check or protected action
The application shell can briefly show stale navigation chrome. Protected queries, route handlers, Server Actions, and commands must use authoritative access checks. The proxy checks only for a cookie and redirects missing cookies. It does not decide access.
React cache belongs to the application's render adapter. Backend packages receive no memoizer.
The app can reuse a completed read within one render. Each backend operation checks its own
permissions and required freshness. Never retain authorization results across requests or reuse
them for commands. ADR 0002
records render-local reuse and per-request access checks.
Server-render reads disable renewal with disableRefresh: true. The signed-in shell mounts Better
Auth's session hook, which renews cookies through the existing handler on mount, focus, reconnect,
and authentication events. No interval polling or navigation-triggered renewal is configured.
The database-backed deployment does not use Better Auth's stateless refreshCache option.
A concurrent revocation takes effect on the next request. Streamed page errors need not preserve literal HTTP 403 or 404 statuses, but must show the correct denial screen without protected data. API status contracts remain unchanged.
Read the Better Auth performance guide and options reference before changing this split.
Stream slow work
Put a Suspense boundary around a slow database or network section. The fallback must match the
loaded shape and set aria-busy="true" with a screen-reader-only loading label. Add a route-level
loading.tsx when the page can wait before it renders.
Prefer a small number of meaningful boundaries. Keep access checks outside the boundary when the check controls whether the page exists.
Show action feedback
Every action that can wait for a Server Action, fetch, or navigation needs immediate feedback:
- Use
useFormStatusin a submit button inside a Server Action form. - Use local pending state for client fetches and native forms.
- Show a Spinner and a useful label such as
Saving…,Deleting…, orOpening…. - Disable only the submitted control or row when that does not cancel browser navigation. Native
navigation forms stay submit-capable and set
aria-busyon the form. - Restore the control and show an error when a client request fails.
Buttons that only open a local menu, disclosure, or copy already-loaded text do not need a network pending state.
Show navigation feedback
Use a route loading.tsx or a nested Suspense skeleton for page transitions. Internal links also
use the shared pending-link pattern. It reserves a fixed-size hint and reveals it after a short delay
with useLinkStatus, so fast navigations stay quiet and slow navigations remain clear.
Use useTransition around client-side router.push calls and show a local status near the initiating
control. Keep protected-route prefetch rules intact. Test the slow path in a browser when you change
these contracts.
How this is checked
| Rule | Checked by |
|---|---|
A use cache function does not read a request API | The Next.js compiler rejects the read |
| Cache boundaries, invalidation tags, and feedback on new code | Code review, against this page |
| The slow path shows its pending and loading states | A browser check when you change one of these contracts |