Configuration
This page is a reference, not a tutorial. For a first working edit, start with Quickstart.
Start with a valid configuration
Section titled “Start with a valid configuration”This example can be pasted into an Astro configuration. Optional providers and collection settings are described below; you do not need every option.
import { defineConfig } from 'astro/config';import caret from '@caretcms/core';
export default defineConfig({ integrations: [caret({ delivery: 'auto', editorHome: '/', brand: { name: 'My site' }, locale: 'en', })],});Options at a glance
Section titled “Options at a glance”| Option | Default | Type |
|---|---|---|
delivery |
'auto' |
'auto' | 'static' | 'server' | { mode?, bake?, publish? } |
mode |
'embedded' |
'embedded' | 'cloud' |
mountPath |
'/admin' |
string |
apiBasePath |
'/api/cms' |
string |
editorHome |
'/' |
same-origin absolute path used after login |
enableAdmin |
true (embedded), false (cloud) |
boolean |
enableInlineEditor |
true (embedded), false (cloud) |
boolean |
storage |
markdownStorage() if src/content/ collections exist, else filesystemStorage() |
CaretStorageProvider |
uploads |
localUploads() |
CaretUploadProvider |
identity |
— | CaretIdentityProvider (embedded mode only) |
deployment |
— | CaretDeploymentProvider for verified build status |
theme |
'studio' |
Preset/module name or { preset?, tokens? } |
brand |
— | { name?, logo?, faviconUrl? } |
cloud |
— | CaretCloudOptions (required when mode: 'cloud') |
schemas |
{} |
Record<string, JsonSchemaDefinition> |
collections |
{} |
Record<string, CollectionStudioConfig> |
locale |
'en' |
'en' | 'es' |
dictionary |
{} |
partial Studio message map |
bodyEditing |
true |
boolean |
allowedClasses |
{} |
Record<string, string[]> — per-tag class allowlist for data-caret-rich |
Unknown option keys log a warning with a did-you-mean hint instead of silently using defaults.
Delivery
Section titled “Delivery”Controls how CMS content reaches visitors. See Static delivery and Deployment.
| Value | Behavior |
|---|---|
Omitted, 'auto', or { mode: 'auto' } |
Resolves from Astro output: static builds bake HTML; server output uses per-request middleware |
'server' or { mode: 'server' } |
Per-request middleware rewrite — requires output: 'server' (+ adapter) |
'static' or { mode: 'static' } |
Dev: admin + API. Build: write published content into HTML; no CMS authoring routes in production |
delivery.bake |
Default true when mode is static — set false to skip HTML rewrite at build |
delivery.publish.webhookUrl |
Optional URL called after Publish to trigger CI rebuild |
delivery.publish.method |
POST (default) or PUT |
delivery.publish.headers |
Extra request headers; keep credential values out of committed config |
delivery.publish.timeoutMs |
Default 5000 ms; bounded to 1–30000 ms |
// Shorthandcaret({ delivery: 'static' })
// With rebuild webhookcaret({ delivery: { mode: 'static', publish: { webhookUrl: process.env.CARET_REBUILD_WEBHOOK_URL }, },})- Default:
'embedded' - Values:
'embedded'|'cloud'Alpha
embedded runs the middleware, admin UI, and API routes inside the Astro site on
the same origin. It is the supported production mode in version 0.5.0.
cloud is alpha. The integration accepts the option and ships a client bootstrap, but the hosted control plane isn’t live yet.
Routes
Section titled “Routes”mountPath
Section titled “mountPath”Where the admin UI mounts. Login is ${mountPath}, Studio is ${mountPath}/cms.
caret({ mountPath: '/staff' })// → /staff, /staff/cmsUse this if /admin collides with your app’s existing routes.
apiBasePath
Section titled “apiBasePath”Base path for all API routes. Every endpoint hangs off this:
${apiBasePath}/entries${apiBasePath}/mutate${apiBasePath}/schema${apiBasePath}/auth/login- …etc
If your project’s src/pages/api/ already has CMS-adjacent routes, change this to avoid collisions.
editorHome
Section titled “editorHome”The same-origin path used after a successful login when the URL has no explicit
?redirect=. It defaults to /, putting editors on a live page where inline
editing is available.
Toggles
Section titled “Toggles”enableAdmin
Section titled “enableAdmin”Whether to inject the admin routes (mountPath, mountPath/cms, mountPath/cms/[collection], mountPath/cms/[collection]/[id]). Set to false to disable Studio entirely while keeping the API surface (e.g. for a pure-headless setup or to hide Studio in prod).
enableInlineEditor
Section titled “enableInlineEditor”Whether to inject the inline-editor bootstrap script onto pages. Disable to turn
off data-caret and data-caret-md page editing while keeping Studio working.
Useful in production if editing should happen only through the admin UI.
bodyEditing
Section titled “bodyEditing”Controls automatic data-caret-md stamping for supported prose rendered from
markdownStorage(). It defaults to true; set it to false when .md body
content should remain read-only. It has no effect on non-Markdown storage or
cloud mode.
Storage and uploads
Section titled “Storage and uploads”storage
Section titled “storage”Where entries, revisions, history, and dynamic-collection metadata live. See Storage Adapters.
If you don’t set this, CaretCMS picks a default: markdownStorage() when your project has Astro content collections under src/content/, otherwise filesystemStorage(). Set it explicitly to override the auto-detection.
import { filesystemStorage } from '@caretcms/core';
filesystemStorage({ dataRoot: 'src/cms-content', // optional — default: '.caret/data' metaRoot: '.caretcms', // optional — default: '.caretcms'})import { cloudflareDurableStorage } from '@caretcms/cloudflare';
cloudflareDurableStorage({ binding: 'CMS_CONTENT', instanceName: 'production' })Requires a Worker class export and SQLite namespace. Follow the Durable Object setup.
import { cloudflareStorage } from '@caretcms/cloudflare';
cloudflareStorage({ binding: 'CMS_KV' }) // default binding: 'CMS_KV'import { defineStorageProvider } from '@caretcms/core';
defineStorageProvider({ entrypoint: './src/cms/postgres-adapter.ts', exportName: 'postgresStorageProvider', options: { connectionStringEnv: 'DATABASE_URL' },})Provider options are serialized into generated modules. Pass an environment variable name for a credential and read its value inside the runtime provider. Do not put credential values in provider options.
identity
Section titled “identity”Shared-password login is the default; production still requires credentials. Server deployments can instead delegate authentication to an authoritative identity provider:
import caret, { defineIdentityProvider } from '@caretcms/core';
caret({ identity: defineIdentityProvider({ entrypoint: './src/caret-identity.ts', exportName: 'identityProvider', options: { loginOrigin: 'https://login.example.com' }, }),});The module exports a factory returning an IdentityAdapter with
authenticate(request), loginUrl({ request, redirectTo }), and an optional
logoutUrl(...). Returning an identity grants editor access; returning null
denies it. Identity mode is authoritative and never falls back to the shared
password. An optional authorize method adds a write policy.
See Authentication & Security.
uploads
Section titled “uploads”Where uploaded files (images via <img data-caret>) go. Same shape as storage.
import { localUploads } from '@caretcms/core';
localUploads({ uploadsDir: './public/uploads' }) // default: 'public/uploads'import { r2Uploads } from '@caretcms/cloudflare';
r2Uploads({ binding: 'CMS_R2', // default: 'CMS_R2' publicBaseUrl: 'https://cdn.example.com', // optional; falls back to the R2_PUBLIC_DOMAIN env var})import { defineUploadProvider } from '@caretcms/core';
defineUploadProvider({ entrypoint: './src/cms/s3-uploads.ts', exportName: 's3UploadHandler', options: { bucket: process.env.S3_BUCKET },})Your entrypoint exports an instance or factory for an UploadHandler (an
upload(file, ctx) method returning { url }), the same way defineStorageProvider wires a custom storage adapter.
R2 uploads require publicBaseUrl or R2_PUBLIC_DOMAIN. Caret rejects the
upload before writing when it cannot return a public image URL; it does not
mount an R2 proxy.
Deployment status, theme and branding
Section titled “Deployment status, theme and branding”deployment accepts defineDeploymentProvider(...) or githubDeployment(...)
to verify the published build. simulatedDeployment(...) is for local testing.
See Deployment status for configuration and evidence rules.
theme controls Studio chrome; brand supplies its name, logo, and favicon.
See Theming for token names and branding examples.
Required when mode: 'cloud'. Ignored otherwise. Cloud mode is alpha and the
hosted control plane is not generally available; these fields are a provisional
client contract, not a production service guarantee.
| Field | Default | Notes |
|---|---|---|
endpoint |
— | Hosted control plane URL. Required. |
projectId |
— | Project ID in the hosted control plane. Required. |
contentPath |
'/content' |
API base path on the hosted endpoint. |
publicToken |
undefined | Public bootstrap token (per-project). |
environment |
undefined | Environment name ('preview', 'production'). |
Schemas
Section titled “Schemas”Map of collection name → JSON Schema. Highest priority schema source. See Schemas.
import { schemaFromZod } from '@caretcms/zod';
caret({ schemas: { pages: schemaFromZod(PageSchema), site: schemaFromZod(SiteSchema), },})Collection presentation and capabilities
Section titled “Collection presentation and capabilities”collections configures how a collection appears and which mutations Studio
allows. These rules are enforced by the server as well as hidden or shown in the
UI.
caret({ collections: { posts: { label: 'Articles', entryLabel: 'article', titleField: 'title', thumbnailField: 'cover', subtitleField: 'summary', previewPath: '/articles/{id}', publication: { field: 'published' }, description: 'Editorial posts', icon: 'document', order: 20, creatable: true, orderable: true, deletable: true, }, site: { label: 'Site settings', singletonId: 'global', creatable: false, deletable: false, order: 1, }, },})The example assumes a schema with title, cover, summary and a top-level
boolean published field. Collection presentation is optional; it does not add
missing fields to your schema.
| Setting | Purpose |
|---|---|
label, description, icon |
Collection identity in Studio |
entryLabel |
Singular name for creation actions, such as article |
titleField |
Field used for entry recognition and title-first creation |
thumbnailField, subtitleField |
Supporting image and text in entry lists |
previewPath |
Same-origin path with {id}, or an ID-to-path map for fixed pages |
publication.field |
Schema-declared top-level boolean; only exact true is publicly returned by Caret reads |
creatable, orderable, deletable |
Collection-wide mutation controls |
singletonId |
Fixed entry instead of an entry list |
order |
Collection position on the dashboard |
These controls are distinct from per-identity permissions. See Saving and publishing for how entry visibility works.
Lower order values appear first. A singletonId opens that fixed entry
directly instead of showing an entry list.
Studio language
Section titled “Studio language”Core ships English and Spanish Studio messages. Select one fixed locale and optionally replace individual messages:
caret({ locale: 'es', dictionary: { 'home.intro': 'Administra el contenido del sitio.', },})Rich text (allowedClasses)
Section titled “Rich text (allowedClasses)”Rich fields use data-caret-rich. The sanitizer strips unsafe markup by default; allowedClasses allows CSS classes per tag so styled inline spans survive round-trip editing.
caret({ allowedClasses: { span: ['gold', 'highlight'], strong: ['big'], },})Caretize prints a copy-pasteable allowedClasses snippet when --rich-class finds classes that need to be allowlisted.
Storage default (auto-detection)
Section titled “Storage default (auto-detection)”When you don’t pass storage, CaretCMS checks for Astro content collections under src/content/. If any exist, it defaults to markdownStorage() so Studio lists those collections immediately (frontmatter follows the save mode; supported body prose always requires Publish). Otherwise it uses filesystemStorage() (.caret/data/ JSON).
Override explicitly when you want JSON storage despite having content collections:
import { filesystemStorage } from '@caretcms/core';
caret({ storage: filesystemStorage() })See Storage Adapters for markdown vs filesystem tradeoffs.
Environment variables
Section titled “Environment variables”Set runtime secrets in your hosting environment. Local .env values are for
local development; load them explicitly with node --env-file=.env ./node_modules/astro/bin/astro.mjs dev
when starting the Node dev server. Do not commit that file. Examples using process.env in
astro.config.mjs expect variables available to the configuration process (for
example, exported by your shell or CI). Do not assume a value in .env is
already available through process.env while the config is being evaluated.
| Variable | Required | Purpose |
|---|---|---|
CARET_EDIT_PASSWORD |
Production password mode | Editor password — anyone with it can log in |
EDIT_PASSWORD |
Fallback | Same as above; kept for backward compat |
CARET_SESSION_SECRET |
Required in production password mode | HMAC secret for session cookies; rotating it signs out every password session |
CARET_TRUST_PROXY |
Optional | Set true behind a reverse proxy so Secure cookies derive from X-Forwarded-Proto |
CARET_GIT_ON_PUBLISH |
Optional | Set true to git-commit storage after a successful publish |
CARET_DEMO_MODE |
Optional | Set true for a public sandbox where every visitor edits in a private, isolated session. See Demo / Sandbox Mode |
CaretCMS does not read a rebuild-webhook URL from the environment directly. Pass it in config, typically from env:
caret({ delivery: { mode: 'static', publish: { webhookUrl: process.env.CARET_REBUILD_WEBHOOK_URL }, },})See Authentication & Security for how each one is used.
Cloudflare deployments also need bindings declared in wrangler.toml:
| Binding | Purpose |
|---|---|
CMS_CONTENT (or your custom name) |
Durable Object namespace for coordinated content |
CMS_KV (or your custom name) |
Legacy single-writer KV content, or demo upload quota tracking |
CMS_R2 (or your custom name) |
R2 bucket for uploads |
R2_PUBLIC_DOMAIN |
Public R2 hostname when publicBaseUrl is not passed |
Defaults summary
Section titled “Defaults summary”{ mode: 'embedded', mountPath: '/admin', apiBasePath: '/api/cms', editorHome: '/', enableAdmin: true, enableInlineEditor: true, delivery: 'auto', // resolves from Astro output storage: filesystemStorage(), // → .caret/data/ (markdownStorage() if src/content collections exist) uploads: localUploads(), // → public/uploads/ schemas: {}, // → all collections inferred collections: {}, locale: 'en', dictionary: {}, bodyEditing: true, allowedClasses: {},}Common combinations
Section titled “Common combinations”caret({ delivery: { mode: 'static', publish: { webhookUrl: process.env.CARET_REBUILD_WEBHOOK_URL }, },})caret({ enableAdmin: import.meta.env.PROD === false, enableInlineEditor: import.meta.env.PROD === false,})caret({ mountPath: '/cms-admin', apiBasePath: '/api/cms-admin',})caret({ storage: cloudflareStorage({ binding: 'CMS_KV' }), uploads: r2Uploads({ binding: 'CMS_R2' }), schemas: { pages: schemaFromZod(PageSchema), },})caret({ enableAdmin: false, enableInlineEditor: false,})API routes still mount; everything UI-related is skipped.