Skip to content

Deployment

CaretCMS runs anywhere Astro does. Pick a delivery mode first, then match storage to your host.

  1. Static deliverycaret({ delivery: 'static' }), bake at build, deploy to a CDN (guide)
  2. Server deliveryoutput: 'server' + adapter, per-request rewrite in production
  3. Storage adapter must match the host’s filesystem semantics
  4. Editor secrets as environment variables

Run through this before any production deploy:

Static delivery (CDN / static host)

  • caret({ delivery: 'static' }) in astro.config.mjs
  • CI runs astro build after publish (webhook or git pull)
  • Optional: CARET_GIT_ON_PUBLISH=true to commit .caret/data/ on publish
  • Build environment can read storage (.caret/data/ in repo or mounted)
  • CARET_EDIT_PASSWORD / CARET_SESSION_SECRET set where authoring runs (dev/staging)

Server delivery (Node / Workers)

  • output: 'server' in astro.config.mjs (+ adapter)
  • CARET_EDIT_PASSWORD set in host env (or editing disabled)
  • CARET_SESSION_SECRET set 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: false and enableInlineEditor: false
HostAdapterNotes
Static CDN (Netlify, Vercel static, S3, Pages)Filesystem (default)Static delivery — bake at build; no CMS routes in prod
Node (Fly, Railway, Render, your VPS)Filesystem (default)Server delivery; mount a persistent volume
Cloudflare WorkerscloudflareStorage + r2UploadsServer delivery; KV + R2 bindings required
Vercel / Netlify (serverless)Custom (Postgres / KV)Server delivery; no writable filesystem

The default path for marketing and brochure sites. No SSR adapter.

  1. Configure static delivery

    astro.config.mjs
    import caret from '@caretcms/core';
    export default defineConfig({
    integrations: [
    caret({
    delivery: {
    mode: 'static',
    publish: {
    webhookUrl: process.env.CARET_REBUILD_WEBHOOK_URL,
    },
    },
    }),
    ],
    });
  2. Author in devnpm run dev, sign in at /admin, edit and publish.

  3. Build and deploy

    Terminal window
    npm run build

    Confirm [caretcms] static delivery bake complete in the logs. Deploy dist/.

  4. Wire CI — on publish, trigger a build that has access to .caret/data/ (committed to git or fetched from staging).

See Static delivery for the full flow.

Once deployed:

  • Visit https://your-site/admin over HTTPS — confirm cookie has Secure flag (devtools → Application)
  • Try logging in with the wrong password — should reject
  • Try POST /api/cms/mutate without the CSRF header — should return 403
  • Try POST /api/cms/mutate without a session cookie — should return 401
  • Confirm /admin is gated (no editor JS on public pages without the cookie)
  • If Cloudflare: check wrangler tail for errors after a save
  • If Node: check the host’s logs for [caretcms] messages on boot

The inline editor runs cleanly under Astro’s strict CSP with no extra configuration:

astro.config.mjs
export default defineConfig({
output: 'server',
security: { csp: true },
integrations: [caret()],
});

Astro auto-hashes the editor’s injected bootstrap, and every other editor asset — editor.js, its module imports, and editor.css — is served same-origin from /__caret/, so the default script-src 'self' and style-src 'self' cover them. The editor’s API calls hit same-origin /api/cms/*, which Astro’s CSP doesn’t restrict. This path is covered by an end-to-end test that asserts zero CSP violations through a full edit-and-save.

Two Astro CSP limitations to know:

  • Production only — Astro emits the policy for astro build (+ preview/deploy), not astro dev. Test CSP against a preview build, not the dev server.
  • No <ClientRouter /> — Astro’s view transitions are not supported with CSP.

If you set CSP yourself via response headers (e.g. at the Cloudflare edge) instead of Astro’s API, allow same-origin sources: script-src 'self', style-src 'self', and connect-src 'self' for the editor’s API calls.

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 with server delivery, no admin routes are mounted when disabled; the rewrite middleware still runs. With static delivery, production is plain static HTML — disable authoring on any server-side preview by setting the toggles from import.meta.env.

The only CaretCMS code on the hot path of every HTML response is the rewrite engine — the order: 'pre' middleware runs it over the rendered body to inject stored overrides. It’s string-based (no DOM parser) and fast: on commodity hardware it adds single-digit microseconds to a page with no editable content, well under 0.1 ms to a typical page (a dozen editable fields), and ~1 ms to a large page with 150 bindings.

Measure it on your own hardware with npm run bench. A regression tripwire in the unit suite fails the build if a large-page rewrite ever blows past a generous ceiling, so a super-linear regression can’t slip in unnoticed.