API
The client your site calls, and the tokens it uses.
Your pages call cms. The keys on a model are Fields. A Next.js app turns a document into a URL in Next.js.
The client
import { cms } from "backmatter";
await cms.listDocs({ type: "blog" });
await cms.query.blog.get(slug); // null when the file is missing
workspaceId and the API URL come from backmatter.config.ts. Tokens stay in the environment: BACKMATTER_API_URL, BACKMATTER_CONTENT_TOKEN, BACKMATTER_PREVIEW_TOKEN. Without the API URL and content token, cms reads content/ from disk.
Typed query
const post = await cms.query.blog.get(slug); // TypedDoc | null
post?.frontmatter.title; // string (required field)
const posts = await cms.query.blog.findMany({
where: { featured: true }, // equality on slug, text, number, boolean
include: { authors: true }, // depth 1; missing → null
});
posts[0]?.authors?.frontmatter.name;
Keys are model.type. A relation is the other document's slug in frontmatter. Querying is the full language.
cms.query trusts the file. Run backmatter validate in CI for files in the repo. Publish checks documents saved in the CMS. Renaming a field in models/blog.ts is a type error wherever you use cms.query.blog. A content file only fails validation when a new field is required.
Validate before push
bunx backmatter validate # models from models/*.ts; exit 1 on invalid frontmatter
bunx backmatter schema # .backmatter/schemas/{type}.json
bunx backmatter generate # .backmatter/* registry (withBackmatter does this for you)
withBackmatter already rewrites .backmatter/ when the Next app compiles. Run generate yourself in CI or outside Next.
validate reads models/*.ts and the folders in each model's path. It does not walk the rest of the repo. A required field that is missing fails the command. Unknown frontmatter keys are kept.
Auth for content
| Token | Use |
|---|---|
BACKMATTER_CONTENT_TOKEN | Published documents on the public site |
BACKMATTER_PREVIEW_TOKEN | The unsaved copy on preview |
The content token is for your site's published reads. The preview token is for the preview page. Neither is the login cookie for the CMS.