Deployment
CaretCMS runs anywhere Astro does. The constraints are:
output: 'server'(or hybrid with editor pages non-prerendered)- The storage adapter has to match the host’s filesystem semantics
- Editor secrets need to be set as environment variables
This page walks through the four common shapes.
Pre-flight checklist
Section titled “Pre-flight checklist”Run through this before any production deploy:
-
output: 'server'inastro.config.mjs -
CARET_EDIT_PASSWORDset in host env (or editing disabled) -
CARET_SESSION_SECRETset to a long random value, not committed to git - HTTPS enforced on your domain
- Storage adapter matches the host (see table below)
- If you don’t want editing on this environment, set
enableAdmin: falseandenableInlineEditor: false
Adapter per host
Section titled “Adapter per host”| Host | Adapter | Notes |
|---|---|---|
| Node (Fly, Railway, Render, your VPS) | Filesystem (default) | Mount a persistent volume; otherwise edits vanish on redeploy |
| Cloudflare Workers | cloudflareStorage + r2Uploads | KV + R2 bindings required |
| Vercel | Custom (Postgres / KV) | No writable filesystem |
| Netlify | Custom (Postgres / KV) | Same — serverless, ephemeral disk |
| Static-site host | Not supported | CaretCMS needs a server |
Pick your host
Section titled “Pick your host”The simplest path. Filesystem storage works as long as you have one process and a persistent disk.
-
Install the Node adapter
Terminal window npm install @astrojs/node -
Configure Astro
astro.config.mjs import { defineConfig } from 'astro/config';import node from '@astrojs/node';import caret from '@caretcms/core';export default defineConfig({output: 'server',adapter: node({ mode: 'standalone' }),integrations: [caret()],}); -
Mount a persistent volume
CMS edits land in
.caret/data/. If your host wipes the filesystem on each deploy, mount a volume there.fly.toml [mounts]source = "cms_data"destination = "/app/.caret/data"Attach a Volume to the service, mount at
/app/.caret/data.Declare a volume on
/app/.caret/datain your compose file or service config. -
Set environment variables
Terminal window CARET_EDIT_PASSWORD=...CARET_SESSION_SECRET=...Set these in your host’s dashboard or CLI. Never commit them.
Use @caretcms/cloudflare for KV storage and R2 uploads. Edge-distributed, no servers to manage.
-
Install
Terminal window npm install @astrojs/cloudflare @caretcms/cloudflare -
Configure Astro
astro.config.mjs import { defineConfig } from 'astro/config';import cloudflare from '@astrojs/cloudflare';import caret from '@caretcms/core';import { cloudflareStorage, r2Uploads } from '@caretcms/cloudflare';export default defineConfig({output: 'server',adapter: cloudflare(),integrations: [caret({storage: cloudflareStorage({ binding: 'CMS_KV' }),uploads: r2Uploads({ binding: 'CMS_R2' }),}),],}); -
Configure
wrangler.tomlwrangler.toml name = "my-site"compatibility_date = "2025-01-01"main = "./dist/_worker.js/index.js"[assets]directory = "./dist"[[kv_namespaces]]binding = "CMS_KV"id = "<your-kv-namespace-id>"[[r2_buckets]]binding = "CMS_R2"bucket_name = "my-site-uploads" -
Set secrets (not in
wrangler.toml)Terminal window wrangler secret put CARET_EDIT_PASSWORDwrangler secret put CARET_SESSION_SECRET -
Deploy
Terminal window npm run buildwrangler deploy
Quirks
Section titled “Quirks”- Eventual consistency — KV is eventually consistent (~60s). After a save, an immediate read might return the old value from another POP. Studio handles this by reading from the same edge that wrote.
- R2 public URLs need a custom domain or
r2.devenabled — check the bucket settings. - CPU limits — Workers have a 30s CPU limit per request. The CMS routes are well under this; if you build heavy custom adapters, keep them async-light.
Vercel and Netlify don’t give you a writable filesystem. Filesystem adapter is out. You need a database-backed custom adapter.
-
Implement a custom adapter
src/cms/postgres-storage.ts import type { StorageAdapter } from '@caretcms/core';class PostgresStorageAdapter implements StorageAdapter {// ...all required methods}export default function postgresStorage() {return new PostgresStorageAdapter(/* config from env */);} -
Wire it up
astro.config.mjs caret({storage: defineStorageProvider({entrypoint: './src/cms/postgres-storage.ts',exportName: 'default',}),}) -
Push uploads to blob storage — S3, R2, or your provider’s blob storage. Implement
UploadHandlersimilarly.
See Storage Adapters → Custom adapter for the full interface.
You can deploy a mostly-static site with the CMS server living on a subdomain or path:
| Surface | URL | Output |
|---|---|---|
| Public site | https://example.com | Fully prerendered, no editing |
| CMS API + admin | https://cms.example.com | output: 'server', hosts admin + API |
Point the editing site at the CMS API via CORS (though right now the integration doesn’t ship CORS headers — you’d need to add a middleware). This is more work than just running one server, but useful when SEO/static perf is non-negotiable.
Hardening checklist
Section titled “Hardening checklist”Once deployed:
- Visit
https://your-site/adminover HTTPS — confirm cookie hasSecureflag (devtools → Application) - Try logging in with the wrong password — should reject
- Try
POST /api/cms/mutatewithout the CSRF header — should return 403 - Try
POST /api/cms/mutatewithout a session cookie — should return 401 - Confirm
/adminis gated (no editor JS on public pages without the cookie) - If Cloudflare: check
wrangler tailfor errors after a save - If Node: check the host’s logs for
[caretcms]messages on boot
Disabling editing in prod
Section titled “Disabling editing in prod”If your prod environment shouldn’t be editable (you only edit on staging, then promote):
caret({ enableAdmin: import.meta.env.MODE === 'staging', enableInlineEditor: import.meta.env.MODE === 'staging',})In prod, no admin routes are mounted, no inline editor JS is injected. Pages still render stored content (the rewriting middleware still runs); they just can’t be edited from the browser.