Configuration
The cms.config.ts file is the single entry point for all CMS configuration.
Everything is configured through a single cms.config.ts file. The engine loads this file at startup and uses it to build the database schema, register API routes, set up permissions, and configure the admin UI.
Config structure
import { config, entity, role, text, slug, select, relation, object, blocks } from "@cms/config";
export default config({
// Locales for i18n (optional)
i18n: {
locales: [
{ label: "English", value: "en" },
{ label: "Portuguese", value: "pt" },
{ label: "French", value: "fr" },
],
defaultLocale: "en",
},
// CORS origins for external frontends (optional)
cors: {
origins: ["https://example.com"],
},
// Media storage adapter (optional, defaults to local)
storage: { adapter: "local" },
// For S3: { adapter: "s3", bucket, region, accessKeyId, secretAccessKey, endpoint? }
// Your content entities — add global: true for singleton entries
entities: [posts, pages, authors, siteSettings],
// Roles and permissions
roles: [admin, editor, publicRole],
});
Built-in entities
CMS automatically registers four built-in entities that you don't need to define:
_user
// Auto-registered with these fields:
// - email (text, unique, required)
// - name (text)
// - role (text, required)
// Plus automatic: id, _type, createdAt, updatedAt
Authentication uses magic-link email codes — no password column. If you need a credential column for a custom auth scheme, add a field on a derived role and write to it from a beforeCreate hook (clients never get to set it).
_image, _video, _file
The three asset entities. All uploads go through POST /api/assets/upload and are routed to the correct entity by MIME type.
// _image — fields: filename, mimeType, size, width, height, alt, url
// _video — fields: filename, mimeType, size, width, height, url
// _file — fields: filename, mimeType, size, url
// All include automatic: id, createdAt, updatedAt
Reference them in your entity config with the typed field helpers:
import { image, video, file } from "@cms/config";
image("featuredImage")
video("demoVideo")
file("attachment", { multiple: true })
See the Assets guide for the full upload API, image transforms, and querying reference.
Automatic fields
Every entity automatically gets:
id— UUID primary key_type— set to the entity name (e.g."posts","authors")createdAt— timestamp, set on insertupdatedAt— timestamp, set on insert and update
You never define these — they're always present.
Admin UI configuration
The admin UI is configured through the admin key:
import { BookOpenIcon, GearIcon } from "@phosphor-icons/react";
import { MyDashboard } from "./my-dashboard";
export default config({
admin: {
logo: "/logo.svg",
title: "My CMS",
sidebar: ({ group, item, divider, builtins }) => [
builtins.DASHBOARD,
group("Content", {
collapsible: true,
children: [
item("posts", { label: "Posts", icon: BookOpenIcon }),
item("authors", { label: "Authors" }),
],
}),
divider(),
item("settings", { label: "Settings", icon: GearIcon }),
divider(),
builtins.ASSETS,
],
dashboard: MyDashboard,
},
// ...
});
See Admin Extensibility for the full guide.
Entity-level admin config
Each entity can carry its own admin options via the admin key on entity(...):
import { entity, slug } from "@cms/config";
import { adminLivePreview } from "@cms/live-preview";
const page = entity("page", {
admin: {
preview: ({ data }) =>
adminLivePreview({
data,
url: `http://localhost:3001/${data.slug}`,
}),
},
fields: [slug("slug"), /* ... */],
});
preview— a function rendered alongside the form when the editor clicks Preview. Thedataparameter is fully typed to your entity's fields. UseadminLivePreviewfor an iframe with real-timepostMessagesync, or return any customReact.ReactNode.
See Live Preview for the full setup guide.
Field-level conditional UI
Individual fields can react to the rest of the form or the current user via admin.visible and admin.readOnly. Common uses: show a "publishedAt" date only when status === "published", hide an internal-notes field from non-editors, or make approvedBy read-only outside the admin role.
date("publishedAt", {
admin: { visible: ({ data }) => data.status === "published" },
}),
See Field visibility and access for the full pattern, including the difference between hiding in the UI and blocking at the API.
Previous
Installation
Next
Fields