Architecture overview

One Worker, one database, one bucket — and the invariants that keep it that way

Cusp is a single-tenant CMS that lives in one Cloudflare Worker. The public render handler, the admin API, the headless API, and the admin SPA's static assets are all the same deployment. There are no service splits at the data layer: one D1 database, one R2 bucket, one KV namespace.

The data layer

Content and configuration live in D1 (SQLite), accessed through raw prepared statements wrapped by a chunked-write repository. IDs are ULIDs stored as TEXT; timestamps are Unix epoch integers, which makes time-travel queries trivial. Large document bodies spill over to R2 when serialised blocks exceed 200 kB, leaving only the R2 key in the row.

Four bindings carry the data layer:

  • DB — the D1 database (content, schema, settings, sessions, versions).
  • BUCKET — the R2 bucket, prefix-organised: documents, media, rendered, themes, plugins, versions, backups, and site (the generated sitemap, feeds, and robots.txt).
  • KV — sessions fast-path, schema cache, fragments, computed aggregates.
  • ASSETS — the admin SPA bundle, served from Workers Static Assets.

The rest of the platform is reached through further bindings on the same Worker: two Durable Objects (DOC_SESSION and RATE_LIMIT), one binding per Cloudflare Workflow (PUBLISH, UNPUBLISH, SCHEDULE, IMPORT, MIGRATE, EXPORT, INSTALL, RESTORE, SEED, PRUNE, SWEEP_MEDIA, REBUILD_SEARCH), PLUGIN_LOADER for the Dynamic Worker Loader, and EMAIL for transactional mail.

Versioned writes

Every mutation of a versioned entity goes through the versioned-write helper, which records an entity_versions row in the same flow as the live-row mutation. Restoring an old version creates a new version rather than rewriting history, so the audit trail is complete and site-wide time travel can reconstruct the state of the whole site at any past timestamp.

The render path

Rendering is hybrid. At publish time a Workflow pre-renders the page to R2; reads are served from the Cache API with stale-while-revalidate; a Worker overlay handles personalisation. Live server-side rendering is a fallback, not the primary path. Cache invalidation uses surrogate-key purges issued as the last step of the publish workflow.

Stateful coordination and async work

Real-time multi-editor collaboration runs through the DocumentSession Durable Object, which is the single source of truth for a document's live state — no CRDT. Async work runs as Cloudflare Workflows with independently retryable steps: publishing, unpublishing, scheduled publish, imports, field migrations, backups, package installs, version restores, pruning, media sweeps, and search-index rebuilds.

Read on

Each subsystem has its own page:

The decision records explain why each choice was made — start with versioned writes, raw D1 statements, and render and publish.