Storage adapters
Storage determines where entries, revisions, history and collection metadata
live. An adapter is the code that reads and writes that storage. Core supplies
filesystem, Markdown and in-memory adapters. The separate
@caretcms/cloudflare package supplies Durable Object and KV adapters.
Picking an adapter
Section titled “Picking an adapter”| Situation | Pick | Write guarantee |
|---|---|---|
| Existing Astro Markdown collections | Markdown on writable storage | Single writer process |
| JSON content, local authoring or persistent Node server | Filesystem | Single writer process |
| Static public site | Filesystem/Markdown readable by the build | Publish, then rebuild/deploy |
| Cloudflare with concurrent editors | Durable Object content + R2 uploads | Coordinated entry and reorder commits |
| Existing Cloudflare KV site | KV + R2 | One editor at a time; no atomic revision check |
| Multiple Node instances or serverless functions | Custom transactional adapter | Depends on your implementation |
| Tests | In-memory | Process-local; disappears on restart |
Filesystem
Section titled “Filesystem”Without explicit storage, CaretCMS uses JSON files unless it detects Markdown
collections under src/content/.
import caret, { filesystemStorage } from '@caretcms/core';
caret({ storage: filesystemStorage({ dataRoot: '.caret/data', metaRoot: '.caretcms', }),});Entries live at .caret/data/<collection>/<id>.json. Revisions, history and
collection metadata live under .caretcms/; private drafts use .caret/drafts/
with the default roots. For git-owned content, choose a tracked dataRoot such
as src/cms-content. When dataRoot changes, the default drafts directory
changes with it: it is a sibling named drafts (in this example, src/drafts).
Exclude that private directory from commits.
In production, persist content, metadata, drafts and uploaded files across deployments. A shared filesystem is not a distributed lock: run only one writer process or implement transactional storage.
Markdown
Section titled “Markdown”Use the same .md/.mdx files that power Astro content collections:
import caret, { markdownStorage } from '@caretcms/core';
caret({ storage: markdownStorage({ contentRoot: './src/content' }) });Studio fields edit frontmatter. Supported .md body prose can be edited on the
page and always requires Publish. Frontmatter follows the save mode:
ordinary server saves can write live; static and policy-controlled saves draft.
The adapter preserves untouched top-level frontmatter blocks and body bytes. Changed fields may be reserialized. See Frontmatter for the YAML subset and Markdown body editing for supported prose and paragraph restructuring.
Use flat collection directories. Nested-slug subfolders are not supported. MDX frontmatter is editable, but MDX/JSX body editing is not. This adapter needs writable source files and does not implement anonymous demo-session overlays.
Cloudflare Durable Objects
Section titled “Cloudflare Durable Objects”import caret from '@caretcms/core';import { cloudflareDurableStorage, r2Uploads } from '@caretcms/cloudflare';
caret({ storage: cloudflareDurableStorage({ binding: 'CMS_CONTENT', instanceName: 'production' }), uploads: r2Uploads({ binding: 'CMS_R2' }),});A SQLite-backed object owns data, revisions, history and indexes. Revision checks and the associated entry transitions commit together; reorder batches commit together or fail without partial changes. Separate objects hold editor drafts and temporary demo sessions.
Follow the full setup for the Worker export, namespace, SQLite lifecycle and generated Astro deployment config. Changing from KV does not import existing runtime content automatically.
Cloudflare KV
Section titled “Cloudflare KV”import caret from '@caretcms/core';import { cloudflareStorage, r2Uploads } from '@caretcms/cloudflare';
caret({ storage: cloudflareStorage({ binding: 'CMS_KV' }), uploads: r2Uploads({ binding: 'CMS_R2' }),});Declare the KV namespace and R2 bucket in Wrangler. This is the legacy, single-writer option. KV is eventually consistent and has no atomic compare-and-swap for the complete entry/revision/history update. Concurrent editors can silently lose updates. Studio cannot guarantee that reads return to the same edge or bypass this limitation.
Both Cloudflare content adapters can read bundled .caret/data/**/*.json as a
baseline when runtime data does not exist. bundledFallback: false disables
that fallback. Bundled seeds are not a migration or backup of live KV data.
In-memory
Section titled “In-memory”import { InMemoryAdapter } from '@caretcms/core';
const adapter = new InMemoryAdapter();adapter.preload('pages', [{ id: 'home', data: { headline: 'Hello' } }]);Useful for tests; data disappears when the process restarts. Core does not
provide a built-in caret({ storage }) factory for this adapter. A custom
provider can return an instance when needed.
Custom adapter
Section titled “Custom adapter”Use the exported StorageAdapter type as the contract. Required operations
cover entry reads/writes, revisions, history and collection metadata. Optional
methods support atomic commits, editor drafts, demo overlays, source publication,
and deployment receipts. Implement only the modes you intend to offer, and test
their failure behavior.
import caret, { defineStorageProvider } from '@caretcms/core';
caret({ storage: defineStorageProvider({ entrypoint: './src/cms/postgres-adapter.ts', exportName: 'postgresStorageProvider', options: { connectionStringEnv: 'DATABASE_URL' }, }),});Your module exports an adapter instance or a factory returning one. In this example the factory reads the named environment variable at runtime and builds the adapter. Provider options are serialized during build: pass the variable name, not its secret value.
Concurrency contract
Section titled “Concurrency contract”Core’s promise-chain locks serialize writes only within one running server.
An atomic revision increment alone does not coordinate a whole entry write
across processes. A transactional backend should implement commitEntries to
compare revision/existence preconditions and commit content, revisions, history
and indexes together.
| Adapter | Coordination |
|---|---|
| Filesystem / Markdown | One writer process; no cross-process locking |
| In-memory | One process only |
| KV | Best-effort revision checks; concurrent updates are unsafe |
| Durable Object | Atomic entry transitions and multi-entry reorder batches |
| Custom | Your backend transaction and contract implementation |
Static and policy-controlled editing require makeEditorOverlay for private
drafts. Recoverable publication also depends on the adapter’s persistence.
See Publish recovery for multi-entry outcomes.
Field validation
Section titled “Field validation”Core validates mutation paths and schema-constrained values before writes. Adapters should still validate identifiers and storage-specific invariants on reads and writes; do not assume every adapter caller passed through a mutation.
Next: Deployment, Permissions, or Configuration.