Skip to content

Storage adapters

A storage adapter is the layer that persists CMS content. CaretCMS ships four and lets you write your own.

Adapter Use for Persistence
Filesystem Default Local dev, single-server prod, git-tracked content JSON files on disk
Markdown Content collections Editing existing Astro content collections in place YAML frontmatter, plus supported prose in .md files
In-memory Tests, ephemeral previews Process memory (lost on restart)
Cloudflare KV Cloudflare Workers, edge Cloudflare KV namespace
Custom Postgres, Redis, S3, anything else You decide
Situation Pick
Local dev Filesystem or Markdown (auto-detected)
Site built on Astro content collections (src/content/*.md) Markdown
Static CDN (static delivery) Filesystem — .caret/data/ in repo or CI checkout
Single-server prod (Node, Fly, Railway, Render, VPS) Filesystem with persistent volume
Multi-instance Node Custom (Postgres / Redis)
Vercel / Lambda Custom (DB-backed) — no writable filesystem
Cloudflare Workers Cloudflare KV + R2
Tests In-memory

The default. Writes JSON files under .caret/data/ and metadata under .caretcms/.

  • Directory.caret/data/
    • Directorypages/
      • home.json
      • about.json
    • Directorysite/
      • global.json
  • Directory.caretcms/
    • revisions.json optimistic-locking counters
    • Directoryhistory/
      • Directorypages/
        • home.json per-entry history (last 50)
    • Directorycollections/
      • products.json dynamic collection metadata

No setup needed — caret() with no options uses it. The directories are created on first write. Customize roots:

astro.config.mjs
import caret, { filesystemStorage } from '@caretcms/core';
caret({
storage: filesystemStorage({
dataRoot: 'src/content/cms', // git-track content in source
metaRoot: 'src/content/cms/.meta',
}),
})

When to use: local dev always. Production when you have one server (or shared NFS), don’t need horizontal scaling, and like content in git.

When not to use: serverless (Workers, Lambda — they don’t have writable disk), multi-instance deployments without shared storage, or anything edge-deployed.

bumpRevision(collection, id) must atomically increment and return the new value. The mutation engine relies on this for optimistic locking.

Implementation Atomicity strategy
Filesystem Serialized in-process queue (single process only)
KV Single-key writes, last-write-wins (acceptable in practice for content)
Postgres UPDATE ... RETURNING in a single statement
Redis INCR

Field paths come from user input. The mutation engine already rejects prototype-pollution attempts (__proto__, constructor.prototype, etc.) before they reach your adapter. You don’t need to re-validate; just trust the inputs you receive in adapter methods.