SDK
@cairncms/sdk is the official JavaScript client for the CairnCMS API. It works in browsers, Node, Bun, Deno, and any other runtime with a fetch implementation. The package is plain TypeScript and the build ships ESM and CommonJS bundles, so it imports cleanly in modern projects without configuration.
The SDK is built around composition rather than a monolithic client class. You start with a small base client and layer in the capabilities you need: REST requests, GraphQL queries, authentication, static tokens. Each capability is a “composable” that adds methods to the client. This page covers the base client, the four built-in composables, the schema-typing pattern, and the helpers that smooth over common request shapes.
Installation
Section titled “Installation”npm install @cairncms/sdkThe package is named @cairncms/sdk. Substitute pnpm, yarn, or bun for npm if you prefer.
Creating a client
Section titled “Creating a client”import { createCairnCMS, rest } from '@cairncms/sdk';
const client = createCairnCMS('https://cms.example.com').with(rest());createCairnCMS(url, options?) returns a thin client object with three properties:
url— the base URL parsed into aURLinstance.globals— the runtime primitives the SDK uses (fetch,URL,logger). Override them when running in environments that need polyfills or want a custom fetch.with(composable)— adds a composable’s methods to the client. Returns a new client with the merged shape.
The base client by itself has no request methods. You compose it with one or more of the four built-in composables (rest, graphql, authentication, staticToken) to get something useful.
The .with() pattern
Section titled “The .with() pattern”Composables are factory functions. Call them, then pass the result into .with():
import { createCairnCMS, rest, graphql, authentication } from '@cairncms/sdk';
const client = createCairnCMS('https://cms.example.com') .with(authentication()) .with(rest()) .with(graphql());Order matters when composables depend on each other. The rest and graphql composables look for an authentication composable’s getToken method on the client and use it to attach Authorization headers. Apply authentication (or staticToken) before rest and graphql if you want automatic token attachment.
Each .with() returns a new merged client. The original is left untouched, so you can branch off a base client into multiple variants if a project needs them.
import { createCairnCMS, rest, readItems } from '@cairncms/sdk';
const client = createCairnCMS('https://cms.example.com').with(rest());
const articles = await client.request(readItems('articles', { filter: { status: { _eq: 'published' } }, fields: ['id', 'title', 'author.name'], limit: 20,}));rest() adds a single method to the client: request(command). The command argument is a value built from one of the SDK’s command factories. readItems, createItem, updateItem, deleteItem, and similar functions build request descriptors that request() then executes. Each command corresponds to a REST endpoint and accepts the same query options as the underlying API.
The full set of commands lives under @cairncms/sdk as named exports. They cover every system collection (users, files, roles, permissions, settings, dashboards, panels, presets, folders, flows, operations, activity, revisions, notifications, shares), the items endpoint for user collections, server info and health, schema snapshot and apply, and the utility endpoints under /utils. The TypeScript types for query parameters and return shapes are derived from the schema, so editors give accurate completions for fields and filters when a schema is provided (see Schema typing below).
A few API surfaces deliberately have no SDK command in v1:
- Config-as-code endpoints (
/config/snapshot,/config/apply). UsecustomEndpointfrom Helpers below, or call the endpoints directly withfetch. - Realtime / WebSocket subscriptions (
realtime()). Not implemented. - Translation strings CRUD, content versioning, and extensions CRUD. Use
customEndpointif you need them, or hit the REST endpoints directly.
The rest() composable also accepts options:
rest({ credentials: 'include', onRequest: (options) => { /* mutate fetch options */ return options; }, onResponse: (data) => { /* transform response */ return data; },})credentials— passed straight tofetch. Set to'include'when running in a browser and using cookie-based auth across origins.onRequest/onResponse— global hooks invoked once per request. Useful for adding telemetry or normalizing errors across an application.
Per-request hooks are also available on individual commands via helpers.
GraphQL
Section titled “GraphQL”import { createCairnCMS, graphql } from '@cairncms/sdk';
const client = createCairnCMS('https://cms.example.com').with(graphql());
const result = await client.query<{ articles: Article[] }>(` query { articles(limit: 5) { id title } }`);graphql() adds a query<Output>(query, variables?, scope?) method to the client. The scope argument selects between the two GraphQL endpoints:
'items'(default) — posts to/graphql, the user-collection endpoint.'system'— posts to/graphql/system, the system-collection endpoint with the auth and TFA mutations.
The same authentication composable works for GraphQL and REST. Compose authentication() (or staticToken()) before graphql() to get automatic Authorization header attachment on every query.
Authentication
Section titled “Authentication”The authentication composable handles the JWT login flow: login, automatic refresh, logout, and token storage. It is the right choice for interactive sessions where users sign in and the SDK manages the access and refresh tokens for the duration of the session.
import { createCairnCMS, authentication, rest } from '@cairncms/sdk';
const client = createCairnCMS('https://cms.example.com') .with(authentication('json')) .with(rest());
await client.login('user@example.com', '<password>');
const me = await client.request(readMe());authentication(mode, config?) accepts:
mode—'cookie'(default) or'json'. Controls whether the refresh token is stored in an HTTP-only cookie or in the SDK’s storage. Use'json'for non-browser environments and any browser context where you cannot rely on cookies. Use'cookie'for first-party browser apps where the refresh token is best kept out of JavaScript-readable storage.config.storage— a custom storage implementation. The default is in-memory. Override for browser apps that need to persist tokens across reloads (usinglocalStorage,sessionStorage, IndexedDB, or anything else that implements the storage interface).config.autoRefresh—true(default) to schedule automatic token refresh before expiry. Set tofalseto manage refresh manually.config.msRefreshBeforeExpires— milliseconds before token expiry to trigger a refresh. Default30000(30 seconds).config.credentials— passed tofetchfor auth requests. Set to'include'for cookie-mode auth across origins.
The composable adds these methods to the client:
login(email, password, options?)— exchange credentials for tokens. Options includeotp(for TFA-enabled accounts),mode(overrides the composable’s mode), andprovider. Theprovideroption only switches the request path to/auth/login/<provider>for credential-style providers like LDAP or any custom local-style driver. It does not handle browser-redirect flows (OAuth2, OpenID Connect, SAML), which require a redirect through the identity provider that the SDK does not orchestrate. Use the REST endpoints directly for those, then callsetToken()with the resulting access token.refresh()— manually trigger a refresh. Auto-refresh handles this in the background, so explicit calls are rare.logout()— invalidate the refresh token and clear stored credentials.getToken()— returns the current access token, refreshing first if it is about to expire. This is the method thatrest()andgraphql()call internally to attach theAuthorizationheader.setToken(access_token)— set the access token directly. Useful for resuming a session from an externally-stored token.stopRefreshing()— cancel the auto-refresh timer. Useful for cleanup on logout or when tearing down a long-running process.
Static tokens
Section titled “Static tokens”For service accounts and CI scripts, the static-token composable is simpler than the auth flow:
import { createCairnCMS, staticToken, rest } from '@cairncms/sdk';
const client = createCairnCMS('https://cms.example.com') .with(staticToken(process.env.CAIRNCMS_TOKEN)) .with(rest());
const result = await client.request(readItems('articles'));staticToken(token) adds getToken() and setToken() methods that return the configured token. The rest() and graphql() composables then attach it as Authorization: Bearer <token> on every request. There is no refresh flow because static tokens do not expire.
Use authentication() for users; use staticToken() for machines.
Schema typing
Section titled “Schema typing”For type safety across collection names, fields, and query results, pass a Schema type parameter to createCairnCMS:
import { createCairnCMS, rest, readItems } from '@cairncms/sdk';
interface Schema { articles: Article; authors: Author;}
interface Article { id: number; title: string; author: number | Author; status: 'draft' | 'review' | 'published';}
interface Author { id: number; name: string;}
const client = createCairnCMS<Schema>('https://cms.example.com').with(rest());
const articles = await client.request(readItems('articles', { fields: ['id', 'title', 'author.name'],}));// articles is typed: id: number, title: string, author: { name: string }The Schema type maps collection names to their record types. The SDK uses it to validate collection names at compile time and to derive return shapes from the fields option, including dotted paths for relations. When fields includes a relation path, the corresponding nested type appears in the result.
For projects with many collections, generating the schema from the live API is common: run cairncms schema snapshot and convert the YAML to TypeScript interfaces, or write a generator that introspects /schema/snapshot on a build step. The SDK ships no generator of its own.
When Schema is omitted, the client falls back to any for collection records. The runtime behavior is identical; only the editor-time type checking changes.
Helpers
Section titled “Helpers”The SDK exposes a few helpers for shaping requests beyond what the command catalog covers directly.
withToken
Section titled “withToken”Override the token for a single request without affecting the composable’s stored token:
import { withToken, readItems } from '@cairncms/sdk';
await client.request(withToken('<other-token>', readItems('articles')));Useful when a script runs as one user but needs to perform a single action as another (for example, a CI job that uses an admin token but issues a single request as the user being onboarded).
withSearch
Section titled “withSearch”Convert a GET command to a SEARCH request, with the query options moved into the request body:
import { withSearch, readItems } from '@cairncms/sdk';
await client.request(withSearch(readItems('articles', { filter: /* a deeply-nested filter that would overflow the URL */, fields: ['*', 'author.*'],})));Reach for this when a query is large enough to bump up against URL length limits. See SEARCH method for the underlying mechanism.
customEndpoint
Section titled “customEndpoint”Build a request against a custom endpoint that does not have a corresponding SDK command:
import { customEndpoint } from '@cairncms/sdk';
const result = await client.request(customEndpoint<MyResponse>({ path: '/my-extension/endpoint', method: 'POST', body: JSON.stringify({ /* ... */ }),}));Useful for endpoints exposed by custom extensions where a typed command does not exist.
withOptions
Section titled “withOptions”Per-request overrides for fetch options on a command:
import { withOptions, readItems } from '@cairncms/sdk';
await client.request(withOptions(readItems('articles'), { signal: abortController.signal, cache: 'no-store',}));Useful for cancellation through AbortController, per-request cache directives, or anything else that needs fetch options without a global hook.
Globals
Section titled “Globals”The base client uses three runtime primitives, exposed under client.globals:
fetch— defaults toglobalThis.fetch. Override for environments that need a polyfill (older Node versions, Deno with custom permissions) or a custom implementation (instrumentation, retry logic).URL— defaults toglobalThis.URL. Override only in environments where the global is missing.logger— defaults toglobalThis.console. Override to capture SDK warnings in your application’s logging system.
Pass overrides through the second argument to createCairnCMS:
import { createCairnCMS } from '@cairncms/sdk';import nodeFetch from 'node-fetch';
const client = createCairnCMS('https://cms.example.com', { globals: { fetch: nodeFetch as unknown as typeof fetch },});In modern runtimes (Node 22+, Bun, Deno, all evergreen browsers), the defaults work without configuration.
Where to go next
Section titled “Where to go next”- Authentication — the underlying auth endpoints, SSO providers, and TFA flow that the
authenticationcomposable uses. - Filters and queries — the query DSL that command options accept.
- Items — the REST endpoints
readItems,createItem, etc. wrap. - GraphQL — the underlying GraphQL endpoints the
graphqlcomposable posts to.