SSR sessions
Own token cookies and refresh concurrency safely in server-rendered applications.
A
JUse @cavuno/board/server for session-cookie encoding and single-flight refresh patterns. The app, not the shared SDK client, owns each user’s tokens.
Why the server client uses nostore
A module-scoped server client can serve many users. It must never persist one user’s access token in shared SDK storage. Configure storage: 'nostore', keep the bearer pair in Cavuno’s httpOnly session cookie, and pass the current access token on each private call.
12345678910// src/lib/cavuno.server.tsimport { createBoardClient } from '@cavuno/board';import { createSessionRefresher } from '@cavuno/board/server';export const board = createBoardClient({board: process.env.PUBLIC_CAVUNO_BOARD!,auth: { storage: 'nostore' },});export const refreshSession = createSessionRefresher(board);
createSessionRefresher fails during construction when a server client uses persistent storage. That failure prevents a cross-user token leak rather than silently accepting an unsafe configuration.
Read and refresh a session
The helpers consume and return cookie header strings, so the same logic can be connected to Next.js, TanStack Start, Remix, or a Worker response.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647// src/lib/load-board-user.tsimport { isUnauthorized } from '@cavuno/board';import {clearSessionCookie,isExpiringSoon,parseSessionCookie,serializeSessionCookie,} from '@cavuno/board/server';import { board, refreshSession } from './cavuno.server';export async function loadBoardUser(cookieHeader: string | null) {let session = parseSessionCookie(cookieHeader);if (!session) return { user: null, setCookie: null };let setCookie: string | null = null;if (isExpiringSoon(session, Date.now())) {session = await refreshSession(session);if (!session) {return { user: null, setCookie: clearSessionCookie() };}setCookie = serializeSessionCookie(session);}const options = {cache: 'no-store' as const,headers: { authorization: `Bearer ${session.accessToken}` },};try {const user = await board.me.retrieve(undefined, options);return { user, setCookie };} catch (error) {if (!isUnauthorized(error)) throw error;const rotated = await refreshSession(session);if (!rotated) {return { user: null, setCookie: clearSessionCookie() };}const user = await board.me.retrieve(undefined, {...options,headers: { authorization: `Bearer ${rotated.accessToken}` },});return { user, setCookie: serializeSessionCookie(rotated) };}}
Attach setCookie to the framework response only when it is non-null. In a multi-board host, pass the same { board: identifier } option to parseSessionCookie, serializeSessionCookie, and clearSessionCookie so sessions cannot collide.
Verify the flow
- Log in, reload, and confirm the account remains authenticated.
- Force the access token near expiry and issue concurrent private reads. They must share one in-process refresh and receive the rotated cookie.
- Revoke or corrupt the refresh token. The next request must clear the cookie and continue signed out without a refresh loop.
- Confirm private responses use
Cache-Control: privateorno-storeat every shared cache.
Production notes
- The SDK cookie codec emits
Path=/; HttpOnly; Secure; SameSite=Laxand uses the__Host-prefix. - Single-flight refresh is per process or isolate. Proactive refresh reduces cross-instance races but cannot eliminate them.
- Refresh tokens are single-use. Retry a protected request at most once after rotation.
- Never serialize a refresh token into HTML, client props, logs, or analytics.
Continue with Caching to keep authenticated responses out of shared caches.