TanStack Start
Use the SDK in TanStack Start loaders and server functions with cookie-owned sessions.
A
JPut Cavuno calls in TanStack Start server functions and load them from routes. This keeps the API boundary explicit, keeps bearer tokens out of client bundles, and lets route components receive serializable data.
The Cavuno reference starter uses TanStack Start on Cloudflare Workers, but the SDK surface is the same on other TanStack Start hosts.
Prerequisites
- A project with
@tanstack/react-startconfigured. @cavuno/boardinstalled.- Your
pk_…publishable key from Settings → Developer → SDK in the Cavuno dashboard. The SDK uses Cavuno’s production API automatically.
Running npx @cavuno/board setup in this project also installs the version-matched cavuno-board-tanstack-start skill.
1. Configure the server client
Add the canonical values to the environment used by your server deployment:
1PUBLIC_CAVUNO_BOARD=pk_your_32_character_hex_key
Create src/lib/cavuno.ts:
123456789101112131415import { createBoardClient } from '@cavuno/board';import { createServerOnlyFn } from '@tanstack/react-start';export const getBoard = createServerOnlyFn(() => {const boardKey = process.env.PUBLIC_CAVUNO_BOARD;if (!boardKey) {throw new Error('Set PUBLIC_CAVUNO_BOARD');}return createBoardClient({board: boardKey,auth: { storage: 'nostore' },});});
createServerOnlyFn makes an accidental browser call fail instead of silently bundling server logic. If your runtime gives every request the same environment values, you can instead export one module-scoped nostore client.
2. Create a public jobs server function
Create src/server/jobs.ts:
12345678import { createServerFn } from '@tanstack/react-start';import { getBoard } from '~/lib/cavuno';export const getJobs = createServerFn({ method: 'GET' }).handler(async () => {const board = getBoard();return board.jobs.list({ limit: 20, sort: 'newest' });});
Server functions are same-origin RPC endpoints, not ordinary local helpers. Keep TanStack Start’s CSRF middleware enabled and apply authorization inside every private server function.
3. Load and render the route
Create src/routes/jobs.tsx:
12345678910111213141516171819202122232425262728293031import { createFileRoute } from '@tanstack/react-router';import { getJobs } from '~/server/jobs';export const Route = createFileRoute('/jobs')({loader: () => getJobs(),component: JobsPage,});function JobsPage() {const { data: jobs } = Route.useLoaderData();return (<main><h1>Open roles</h1><ul>{jobs.map((job) => (<li key={job.id}>{job.company ? (<a href={`/companies/${job.company.slug}/jobs/${job.slug}`}>{job.title} at {job.company.name}</a>) : (job.title)}</li>))}</ul></main>);}
Use the route generator produced by your TanStack configuration as the authority for file names and route IDs. The public URL should remain /companies/:companySlug/jobs/:jobSlug.
4. Read a server-owned session
Create src/server/me.ts:
1234567891011121314151617import { parseSessionCookie } from '@cavuno/board/server';import { createServerFn } from '@tanstack/react-start';import { getRequest } from '@tanstack/react-start/server';import { getBoard } from '~/lib/cavuno';export const getMe = createServerFn({ method: 'GET' }).handler(async () => {const request = getRequest();const session = parseSessionCookie(request.headers.get('cookie'));if (!session) return null;return getBoard().me.retrieve(undefined, {cache: 'no-store',headers: { authorization: `Bearer ${session.accessToken}` },});});
The SDK cookie helpers speak raw Cookie and Set-Cookie strings. Your login/logout server functions own the framework response headers. Keep the bearer pair in one httpOnly cookie and return only the user fields the route needs.
Do not mark getMe or any identity-dependent response as publicly cacheable. Shared caching of a cookie-dependent server function can return one user’s record to another.
5. Refresh before expiry
Create one createSessionRefresher(board) instance in the server module that owns the concrete client. Parse the cookie for each request, call the refresher when isExpiringSoon(session, Date.now()) is true, and write the rotated session back with serializeSessionCookie.
A refresher deduplicates only inside one process or Worker isolate. On a null result, clear the session cookie and continue signed out; never loop on a single-use refresh token.
Verify the integration
- Run TanStack Start’s typecheck and production build.
- Load
/jobswith JavaScript enabled and disabled; the loader should return the same public job data. - Inspect the client bundle and serialized route data—no access or refresh token should appear.
- Call the generated
getMeserver-function endpoint without a cookie and confirm it returnsnull. - Sign in as two users and confirm no server-function cache crosses the session boundary.
Continue with the Cloudflare Workers guide if Workers is your deployment runtime, or SSR sessions for the complete rotation flow.