Skip to content

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.

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

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.

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.

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.

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.

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.

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.

astro.config.mjs
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.

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.

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.