Skip to content

Cloudflare Durable Object storage

cloudflareDurableStorage() stores each CaretCMS site in a SQLite-backed Cloudflare Durable Object. The object is the authority for entry data, revisions, history, collection indexes, and collection metadata.

Use this adapter for a Cloudflare Workers site when multiple requests or editors may save at the same time. It coordinates those writes so one save cannot silently overwrite another save based on an older revision. The legacy KV adapter cannot coordinate those writes. Core submits a validated entry transition through the optional StorageAdapter.commitEntries contract. The Durable Object compares every entry revision and existence state, then commits the complete batch. A conflict changes nothing. Single-entry saves and deletes update data, revision, history, and indexes together; a reorder commits every participating entry together.

Install @caretcms/core@0.5.0, @caretcms/cloudflare@0.5.0, @astrojs/cloudflare, and Wrangler in your Astro project. Configure production authentication and an R2 public URL if using uploads.

Use the Durable Object provider for content. R2 remains the upload store.

import { defineConfig } from "astro/config";
import cloudflare from "@astrojs/cloudflare";
import caret from "@caretcms/core";
import { cloudflareDurableStorage, r2Uploads } from "@caretcms/cloudflare";
export default defineConfig({
output: "server",
adapter: cloudflare(),
integrations: [
caret({
storage: cloudflareDurableStorage({
binding: "CMS_CONTENT",
instanceName: "production",
}),
uploads: r2Uploads({ binding: "CMS_R2" }),
}),
],
});

Astro applications with custom Cloudflare exports need a Worker entrypoint.

src/worker.js
import { handle } from "@astrojs/cloudflare/handler";
export { CaretCmsContent } from "@caretcms/cloudflare/durable-object";
export default {
fetch(request, env, ctx) {
return handle(request, env, ctx);
},
};

Point Wrangler at that entrypoint, bind the namespace, and declare the class as a SQLite-backed Durable Object. Cloudflare’s current configuration also supports legacy migrations for applications that already use them; do not mix the two lifecycle formats.

wrangler.toml
name = "my-caret-site"
compatibility_date = "2026-09-13"
compatibility_flags = ["nodejs_compat"]
main = "./src/worker.js"
[[durable_objects.bindings]]
name = "CMS_CONTENT"
class_name = "CaretCmsContent"
[exports.CaretCmsContent]
type = "durable-object"
storage = "sqlite"
[[r2_buckets]]
binding = "CMS_R2"
bucket_name = "my-site-uploads"

Create the R2 bucket with npx wrangler r2 bucket create my-site-uploads. Set R2_PUBLIC_DOMAIN to its enabled public hostname (or pass publicBaseUrl to r2Uploads). Use Worker secrets for the editor password and session secret.

Run astro build before Wrangler. Astro bundles the custom entrypoint and emits dist/server/wrangler.json; deploy that generated configuration so Wrangler uses Astro’s resolved virtual modules and asset paths.

Terminal window
npm run build
npx wrangler deploy --config dist/server/wrangler.json

See Cloudflare’s documentation for Durable Object class lifecycle and Astro’s custom Cloudflare entrypoint.

instanceName selects the site’s coordination object. Use a stable, unique name per tenant or independently managed site. Caret creates separate object names for each persistent editor draft and each temporary demo session, so unrelated drafts do not serialize through the published-content object. Demo-session objects refresh a two-hour alarm and erase their storage when it fires.

Bundled .caret/data remains a read-only baseline by default. The first edit materializes the changed entry in the object; deletions use durable tombstones so the build-bundled entry does not reappear. Set bundledFallback: false for an empty object-backed site.

The adapter does not copy existing runtime values from CloudflareKvStorageAdapter. Changing providers keeps build-bundled seed content but requires an explicit export/import plan for entries that exist only in KV.

Check the generated Worker uses the intended account, namespace, class and R2 bucket. Exercise saving and history with test content, then verify that two same-revision writes produce one success and one conflict. Check draft/session isolation if those modes are enabled. Use a staging deployment for destructive tests.

The adapter coordinates entry transitions and reorder batches within an object. This does not turn a multi-entry publication into one global transaction; review individual publish outcomes.