Skip to content

Local API

Import the generated API and call it directly without HTTP overhead. All queries and return types are fully typed based on your collection definitions.

import { cms } from "@/cms/.generated/api";

Every collection is available as cms.<collection-slug>. For example, if you have collections posts, pages, and users:

cms.posts.find({ ... })
cms.pages.findOne({ slug: "about" })
cms.users.findById("abc123")
const posts = await cms.posts.find({
where: { category: "tech" },
sort: { field: "_updatedAt", direction: "desc" },
limit: 10,
offset: 0,
status: "published",
locale: "fi",
});
Option Type Default Description
where Record<string, unknown> Filter by field values
sort { field, direction } Sort by field, "asc" or "desc"
limit number Max documents to return
offset number Skip N documents
status "draft" | "published" | "scheduled" | "any" "published" (draft-enabled collections) / "any" (others) Filter by status
locale string default locale Language code for translations
search string Substring search across text, slug, email, and select fields
const post = await cms.posts.findOne({
slug: "hello-world",
locale: "fi",
status: "any",
});
Option Type Default Description
field filters varies Filter by any field (e.g. slug, email)
locale string default locale Language code
status "draft" | "published" | "scheduled" | "any" "published" (draft-enabled collections) / "any" (others) Status filter
const post = await cms.posts.findById("abc123", {
locale: "fi",
status: "any",
});
Option Type Default Description
locale string default locale Language code
status "draft" | "published" | "scheduled" | "any" see below Status filter

findById filters by status only when status is passed explicitly: a value other than "any" returns null unless the document has that status. By default the document is returned regardless of status. For draft-enabled collections, the default also overlays the last-published snapshot values on the result (the same happens with an explicit "published"). Pass status: "any" to read the current working values without filtering.

const post = await cms.posts.create({
title: "New Post",
body: { type: "root", children: [...] },
_status: "published", // optional, defaults to "draft" if drafts enabled
});

Pass field values as properties. Returns the created document. You can supply your own _id to make imports idempotent; otherwise one is generated.

const posts = await cms.posts.createMany([{ title: "A" }, { title: "B" }]);

Runs the documents through create sequentially and returns the created documents. See Migrations for bulk-import patterns.

await cms.posts.upsert({ _id: "abc123", title: "Hello" });

Updates when a document with data._id exists, otherwise creates. Combined with caller-supplied _ids this makes imports re-runnable without a wipe-first step.

const post = await cms.posts.update("abc123", {
title: "Updated Title",
});

Only include fields you want to change. Returns the updated document.

await cms.posts.delete("abc123");

Cascades: removes translations, versions, and the document. Returns true if a row was removed, false if the document was not found. Auth collections refuse to delete the last remaining admin (the call throws).

const removed = await cms.posts.deleteMany({ category: "tech" });

Deletes every document matching the filter (all documents when the filter is omitted) and returns the number removed. Like discardDraft, it is runtime-only and not part of the generated typed API surface.

await cms.posts.publish("abc123");
await cms.posts.unpublish("abc123");
await cms.posts.discardDraft("abc123");

Reverts a published document’s pending changes back to the last-published content. Only works on published documents that have been edited.

Note: discardDraft is currently runtime-only — it is not part of the generated typed API surface, so TypeScript will flag the call even though it works at runtime.

await cms.posts.schedule(
"abc123",
"2025-06-01T00:00:00Z", // publishAt (required)
"2025-07-01T00:00:00Z", // unpublishAt (optional)
);

Sets status to "scheduled". On Cloudflare, a cron trigger processes scheduled publishing automatically. For Node.js, set up an external cron to call /api/cms/cron/publish. Set the CRON_SECRET env var to secure the endpoint.

const total = await cms.posts.count({ status: "published" });

Accepts same filter options as find (without limit, offset, sort).

const versions = await cms.posts.versions("abc123");
// → [{ version: 5, createdAt: "...", snapshot: {...} }, ...]
await cms.posts.restore("abc123", 5);
const translations = await cms.posts.getTranslations("abc123");
// → { fi: { title: "...", body: {...} } }
await cms.posts.upsertTranslation("abc123", "fi", {
title: "Hei maailma",
body: { type: "root", children: [...] },
});

upsertTranslation inserts or updates. Only include translatable fields.

Every method accepts an optional context object as its last argument. Pass the signed-in admin user to enforce field-level access, or _system: true to bypass all access rules (for every operation: create/update/delete/read) in trusted server code such as public API routes, task handlers, and seed scripts:

await cms.posts.find({}, { user });
await cms["form-submissions"].create(data, { _system: true });

_skipSearch: true is a runtime-only bulk-import flag that skips per-document search indexing. It is not part of the generated context type.

cms.meta.getCollections(); // All collections with metadata
cms.meta.getFields("posts"); // Field definitions for a collection
cms.meta.getCollection("posts"); // Full collection config
cms.meta.getRouteForDocument("posts", doc); // Public URL for a document
cms.meta.getLocales(); // { default, supported }
cms.meta.isTranslatableField("posts", "title"); // true/false
cms.meta.getConfig(); // Full CMS config object

Background task helpers also exist as cms.tasks.enqueue, cms.tasks.drain, cms.tasks.tick, and cms.tasks.prune. See Background Tasks.

---
import { cms } from "@/cms/.generated/api";
const post = await cms.posts.findOne({ slug: Astro.params.slug });
if (!post) return Astro.redirect("/404");
Astro.cache.set({ tags: ["posts", `post:${post._id}`] });
---
<h1>{post.title}</h1>

The raw tag array is equivalent to the cacheTags() helper used in Public Pages.