Authentication and session ownership

Choose browser storage or an SSR httpOnly-cookie session without leaking bearer tokens.

Board users authenticate with a short-lived access token and a single-use refresh token. Decide where that pair lives before adding login: the correct design differs between a browser-only application and a server-rendered application.

The SDK uses bearer tokens only. It does not create a Cavuno cookie session for you.

Choose the session owner

ApplicationSession ownerSDK storageHow authenticated requests receive the token
Browser-only SPAThe SDK in that browsermemory, session, or localRead automatically from SDK storage
SSR or server frameworkYour application’s httpOnly cookienostoreauthorization header in each call’s trailing options

The client defaults to memory when a browser document exists and nostore elsewhere. Set the mode explicitly when the runtime boundary should be obvious to reviewers.

Browser-owned sessions

Create a browser client with the persistence that matches your product:

ts
import { createBoardClient } from '@cavuno/board';
const board = createBoardClient({
board: 'pk_0123456789abcdef0123456789abcdef',
auth: { storage: 'session' },
});
const session = await board.auth.login({
email: 'ada@example.com',
password: 'a-strong-password',
});
const me = await board.me.retrieve();
console.log(me.email === session.boardUser.email);

login, register, consumeMagicLink, exchangeOAuth, and refresh write the returned pair to the configured storage. Subsequent calls read the latest access token and attach it as a bearer automatically.

Browser storage modes

  • memory keeps tokens only in that client instance. A reload signs the user out.
  • session uses sessionStorage, scoped to the tab.
  • local uses localStorage, persisting across reloads and tabs.
  • A CustomStorage supplies async getItem, setItem, and removeItem methods.

Persistent Web Storage is accessible to JavaScript, including any successful cross-site scripting payload. Prefer memory or session unless persistence is worth that exposure. local and session throw at client creation outside a browser or when the browsing context blocks storage, such as some sandboxed iframes.

The SDK automatically scopes Web Storage keys by board, so two boards on one origin do not share tokens.

Server-owned sessions

Keep one module-scoped client stateless:

ts
import { createBoardClient } from '@cavuno/board';
export const board = createBoardClient({
board: process.env.PUBLIC_CAVUNO_BOARD!,
auth: { storage: 'nostore' },
});

After a server-side login, write the returned pair to a cookie your application owns. The server helpers produce cookie strings without depending on a framework:

ts
import { serializeSessionCookie } from '@cavuno/board/server';
const session = await board.auth.login({ email, password });
const setCookie = serializeSessionCookie({
accessToken: session.accessToken,
refreshToken: session.refreshToken,
expiresAt: session.expiresAt,
});

serializeSessionCookie uses a __Host- name with Path=/, HttpOnly, Secure, SameSite=Lax, and a 30-day maximum age. Your route handler or server function is responsible for putting that string on the response. The codec stores JSON, not encrypted data; wrap it if your security model requires cookie confidentiality.

On each request, parse the incoming Cookie header and pass authorization only to that user’s API call:

ts
import { parseSessionCookie } from '@cavuno/board/server';
const session = parseSessionCookie(request.headers.get('cookie'));
const me = session
? await board.me.retrieve(undefined, {
headers: { authorization: `Bearer ${session.accessToken}` },
})
: null;

Never assign per-user storage to a shared server client. Concurrent requests can otherwise read another user’s token.

Refresh the session explicitly

Access tokens last one hour. Refresh tokens last 30 days, rotate on use, and cannot be reused. A 401 is returned to your code; the SDK never performs a hidden refresh and retry.

In a browser-owned session:

ts
import { isUnauthorized } from '@cavuno/board';
try {
return await board.me.retrieve();
} catch (error) {
if (!isUnauthorized(error)) throw error;
await board.auth.refresh();
return board.me.retrieve();
}

Guard concurrent browser refreshes with one shared in-flight promise. Without it, parallel 401 handlers can both spend the same refresh token.

For server-owned sessions, create one refresher at module scope:

ts
import {
createSessionRefresher,
isExpiringSoon,
} from '@cavuno/board/server';
const refreshSession = createSessionRefresher(board);
if (session && isExpiringSoon(session, Date.now())) {
const rotated = await refreshSession(session);
// Write rotated with serializeSessionCookie, or clear the cookie when null.
}

The refresher deduplicates rotations only within one process or Worker isolate. Separate serverless instances can still race. It returns null after a 401—clear the cookie and continue signed out; do not retry a burned token. Network, rate-limit, and server errors are rethrown.

Log out safely

Browser storage callers can use await board.auth.logout(). A nostore server caller must provide the refresh token:

ts
await board.auth.logout({ refreshToken: session.refreshToken });

Only clear the application cookie after logout succeeds. If the request fails before revocation, the SDK deliberately preserves stored tokens because the refresh token may still be live server-side.

Verify the boundary

  • Inspect browser storage and confirm it matches the chosen browser mode.
  • In SSR, confirm the bearer never appears in rendered HTML, React props, URLs, logs, or public environment variables.
  • Make two authenticated requests for different test users concurrently and verify each returns its own identity.
  • Force an expired access token and verify exactly one refresh occurs within the process.
  • Verify logout revokes the refresh token and expires the application cookie.

Your server functions and mutation routes must still apply the framework’s CSRF protections. The cookie codec does not authorize routes or validate form origins.

Next, learn how to fetch and paginate board data.