createBoardClient

Configure storage, headers, hooks, logging, and the exact Board API request pipeline.

createBoardClient(options) creates the complete typed Board SDK. A client is safe to reuse across public requests. A shared server client is also safe when it uses nostore and receives user credentials per call.

Signature

ts
import {
createBoardClient,
type BoardSdk,
type CreateBoardClientOptions,
} from '@cavuno/board';
const create: (options: CreateBoardClientOptions) => BoardSdk =
createBoardClient;

Options

OptionRequiredExact behavior
baseUrlNoAPI origin. Default: https://api.cavuno.com. Override only when Cavuno supplies a staging or development origin. Trailing slashes are removed before the client appends /v1/boards/{encodedBoard}.
boardYespk_… publishable key, boards_… ID, or slug. The value is URL-encoded in the base path and scopes persistent browser storage.
auth.storageNomemory, nostore, local, session, or a CustomStorage. Default: memory when globalThis.document exists; nostore otherwise.
globalHeadersNoHeaders applied to every request after SDK defaults and before stored or per-call credentials.
onRequestNoSync or async hook that receives the complete URL and RequestInit; it may mutate or replace the request.
onResponseNoSync or async side-effect hook that receives a response clone and the final request before response parsing.
loggerNo{ debug(message), error(message) }. Debug receives request and response lines; error receives a typed API error line.

Create a browser client

ts
// src/lib/cavuno.ts
import { createBoardClient } from '@cavuno/board';
export const board = createBoardClient({
board: 'pk_example',
auth: { storage: 'session' },
});
const context = await board.context();

Storage modes have intentionally different lifetimes:

  • memory is private to this client and disappears on reload.
  • session uses sessionStorage and survives reloads in one tab.
  • local uses localStorage and survives browser restarts.
  • nostore never retains tokens.
  • A custom adapter owns persistence, board scoping, and user isolation.

local and session throw during client creation when used outside a browser or when Web Storage access is blocked. Their keys are scoped by the board identifier so two boards on one origin do not share tokens.

Create a stateless server client

ts
// src/lib/cavuno.server.ts
import { createBoardClient } from '@cavuno/board';
export const board = createBoardClient({
board: process.env.PUBLIC_CAVUNO_BOARD!,
auth: { storage: 'nostore' },
});
export function getMe(accessToken: string) {
return board.me.retrieve(undefined, {
cache: 'no-store',
headers: { authorization: `Bearer ${accessToken}` },
});
}

Store the token pair in an application-owned httpOnly cookie. Never attach one user’s storage adapter to a module-scoped server client.

Use custom storage

ts
import type { CustomStorage } from '@cavuno/board';
const values = new Map<string, string>();
const storage: CustomStorage = {
getItem: (key) => values.get(key) ?? null,
setItem: (key, value) => {
values.set(key, value);
},
removeItem: (key) => {
values.delete(key);
},
};

The SDK reads the access token on every request. Auth methods write and clear the access/refresh pair. Logging out deliberately leaves the board-password grant intact because it is board-scoped, not user-scoped.

Request pipeline

For every board.client.fetch or namespace call, the client performs these steps in order:

  1. Remove trailing slashes from baseUrl and build /v1/boards/{encodedBoard}{path}.
  2. Serialize a flat query. Arrays become repeated keys; null and undefined values are omitted.
  3. Separate body and query; pass every other FetchOptions field through to fetch.
  4. JSON-stringify ordinary bodies and add content-type: application/json. Strings, URLSearchParams, FormData, Blob, ArrayBuffer, and ReadableStream pass through unchanged.
  5. Merge headers in this order: accept: application/json, JSON content type, globalHeaders, stored bearer token, stored X-Board-Access grant, then per-call headers. Later values win.
  6. Add X-Cavuno-Sdk: board@{SDK_VERSION} only when the request is non-GET or already carries authorization/board access. An explicit header is not replaced.
  7. Run and await onRequest, then log and call globalThis.fetch.
  8. Run and await onResponse with a clone, so the hook cannot consume the SDK’s response body.
  9. Return undefined for HTTP 204; parse successful non-204 responses as JSON.
  10. For non-2xx responses, parse the error envelope and throw BoardApiError. An unparseable envelope becomes unknown_error.

The client never automatically retries a request and never refreshes a session on 401.

Hook the pipeline

ts
const board = createBoardClient({
board: 'pk_example',
globalHeaders: { 'x-app-version': '2026.07.10' },
onRequest(request) {
request.init.headers = new Headers(request.init.headers);
request.init.headers.set('x-request-source', 'custom-board');
return request;
},
onResponse(response, request) {
if (!response.ok) {
console.error(response.status, request.url);
}
},
});

Do not log authorization headers, board-access grants, or private response bodies.

Verify the client

Call board.context() and inspect the network request. It should target /v1/boards/{identifier}, include accept: application/json, and omit X-Cavuno-Sdk on an anonymous GET. An authenticated read should include both the bearer and SDK version headers.

Unknown storage modes fail during construction. Board resolution and all other non-2xx request failures use BoardApiError.