Skip to content

Configuration

This page is a reference, not a tutorial. For the path through, start with Quickstart.

astro.config.mjs
import caret from '@caretcms/core';
caret({
// Delivery
delivery: 'auto' | 'static' | 'server' | { mode?: 'auto' | 'static' | 'server', bake?: boolean, publish?: { webhookUrl?: string } },
// Mode
mode: 'embedded' | 'cloud',
// Routes
mountPath: '/admin',
apiBasePath: '/api/cms',
editorHome: '/',
// Toggles
enableAdmin: true,
enableInlineEditor: true,
// Storage and uploads (embedded mode only)
storage: filesystemStorage(),
uploads: localUploads(),
identity: defineIdentityProvider({ entrypoint: './src/caret-identity.ts' }),
// Cloud (cloud mode only)
cloud: {
endpoint: 'https://control-plane.example',
projectId: 'project-id',
},
// Schemas
schemas: { /* collection: jsonSchema */ },
collections: { /* collection: Studio metadata and capabilities */ },
// Studio language and message overrides
locale: 'en',
dictionary: { 'entry.saveLive': 'Publish now' },
// Rendered .md prose
bodyEditing: true,
// Rich text
allowedClasses: { span: ['gold'], strong: ['big'] },
})
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)
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.

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: full admin + API. Build: bake .caret/data into HTML; no CMS routes in prod output
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
// Shorthand
caret({ delivery: 'static' })
// With rebuild webhook
caret({
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.3.

cloud is alpha. The integration accepts the option and ships a client bootstrap, but the hosted control plane isn’t live yet.

Where the admin UI mounts. Login is ${mountPath}, Studio is ${mountPath}/cms.

caret({ mountPath: '/staff' })
// → /staff, /staff/cms

Use this if /admin collides with your app’s existing routes.

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.

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.

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 disable editing in prod).

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.

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.

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/content/cms', // optional — default: '.caret/data'
metaRoot: 'src/content/cms/.meta', // optional — default: '.caretcms'
})

Shared-password login is the zero-configuration default. Server deployments can instead delegate authentication to an authoritative identity provider:

astro.config.mjs
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. See Authentication & Security.

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'

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.

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').

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),
},
})

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',
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,
},
},
})

Lower order values appear first. A singletonId opens that fixed entry directly instead of showing an entry list.

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 fields use data-caret-rich. The sanitizer strips unsafe markup by default; allowedClasses whitelists 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.

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 and supported .md prose edits write back on 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.

Variable Required Purpose
CARET_EDIT_PASSWORD Yes (for editing) 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_KV (or your custom name) KV namespace for storage
CMS_R2 (or your custom name) R2 bucket for uploads
R2_PUBLIC_DOMAIN Public R2 hostname when publicBaseUrl is not passed
{
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: {},
}
caret({
delivery: {
mode: 'static',
publish: { webhookUrl: process.env.CARET_REBUILD_WEBHOOK_URL },
},
})