Your content, on the edge. EdgeCMS is a headless content management system that runs entirely on Cloudflare Workers — no origin servers, no cold starts, no nonsense. Ship i18n, media, and structured content blocks from the fastest infrastructure on the planet.
- Zero-origin architecture — D1 for storage, KV for caching, R2 for media. Everything runs at the edge.
- Drop-in ready — Deploy alongside your existing Cloudflare Workers app.
All routes live under
/edge-cms. - Type-safe SDK — Pull translations, generate TypeScript types, and import content blocks from the CLI. Your IDE stays happy.
- Version control for content — Draft, publish, rollback. Treat your content like code.
- AI-powered translations — Auto-translate missing keys with OpenAI. Ship faster in every language.
- Multi-language support with fallback to default locale
- Inline editing with auto-save — no submit buttons, no friction
- Section-based organization for large translation sets
- Draft/live versioning with publish and rollback
- AI-powered auto-translation for missing keys
- Cached public API endpoints for blazing-fast delivery
- Define block schemas with typed properties (string, number, boolean, translation, media, block, collection)
- Create singleton or multi-instance collections
- Nest blocks within blocks for complex content structures
- Full versioning support — draft, publish, rollback
- Bulk import via CLI for migration workflows
- Upload files to R2 with automatic kebab-case sanitization
- Section-based organization
- Direct streaming from R2 — no intermediary processing
- Media state tracking with draft/live versioning
- Email/password auth powered by Better Auth
- Admin role management with protected routes
- API key support for programmatic access
- Per-key rate limiting (default: 1000 req/hour)
- Usage tracking with last-request timestamps
- Draft and live content states
- Publish drafts with Cloudflare Workflows
- Rollback to any previous version instantly
- Version descriptions for change tracking
| Layer | Technology |
|---|---|
| Framework | React Router v7 |
| UI | Tailwind CSS 4 + shadcn/ui |
| Database | Cloudflare D1 (SQLite) |
| ORM | Drizzle |
| Cache | Cloudflare KV |
| Storage | Cloudflare R2 |
| Auth | Better Auth |
| AI | OpenAI (via AI SDK) |
| Workflows | Cloudflare Workflows |
| Runtime | Cloudflare Workers |
npm installCreate a .dev.vars file for local development:
AUTH_SECRET=your-secret-key-here
ADMIN_SIGNUP_PASSWORD=your-admin-signup-secret
OPENAI_API_KEY=your-openai-api-key # Optional — for AI translationsFor production, set these as Cloudflare secrets:
npx wrangler secret put AUTH_SECRET
npx wrangler secret put ADMIN_SIGNUP_PASSWORD
npx wrangler secret put OPENAI_API_KEYYour wrangler.jsonc needs the following bindings:
# Local
npx wrangler d1 migrations apply edgecms-db --local
# Production
npx wrangler d1 migrations apply edgecms-dbnpm run typechecknpm run devnpm run deployThe @upbeat-works/edgecms-sdk package gives you a CLI and programmatic API to
interact with EdgeCMS from your codebase.
npm install @upbeat-works/edgecms-sdkCreate an edgecms.config.json in your project root:
{
"localesDir": "./src/locales",
"defaultLocale": "en",
"typesOutputPath": "./src/locales/types.ts",
"baseUrl": "${EDGECMS_BASE_URL}"
}Set your API key as an environment variable:
export EDGECMS_API_KEY=your-api-key
export EDGECMS_BASE_URL=https://your-domain.com/edge-cmsPull translations and generate TypeScript types.
edgecms pull # Pull live translations for default locale
edgecms pull --from draft # Pull draft translations
edgecms pull --all # Pull all localesThis generates a types file with full autocompletion:
// Auto-generated by @edgecms/sdk
export interface TranslationKeys {
'common.title': string;
'common.description': string;
'homepage.hero': string;
}
export type TranslationKey = keyof TranslationKeys;
export function t(key: TranslationKey): TranslationKey {
return key;
}Push local translations to EdgeCMS as a draft.
edgecms push # Push default locale translations
edgecms push --section "homepage" # Assign new keys to a sectionBulk import block instances from a JSON file.
edgecms import-blocks ./data.json "hero-blocks"
edgecms import-blocks ./data.json "carousel" --locale "es"import { pull, push, importBlocks } from '@upbeat-works/edgecms-sdk';| Route | Description |
|---|---|
/edge-cms/sign-in |
Authentication |
/edge-cms/sign-up |
Admin registration |
/edge-cms/i18n |
Translation management |
/edge-cms/i18n/versions |
Version management |
/edge-cms/blocks |
Block schema & collection mgmt |
/edge-cms/media |
Media upload & management |
/edge-cms/sections |
Section management |
/edge-cms/users |
User management |
/edge-cms/settings/api-keys |
API key management |
| Route | Description |
|---|---|
GET /edge-cms/public/i18n/:locale.json |
Translations for a locale (cached) |
GET /edge-cms/public/media/:filename |
Serve media files from R2 |
GET /edge-cms/public/blocks/:collection |
Block collection data |
| Route | Method | Description |
|---|---|---|
/edge-cms/api/i18n/pull |
GET | Fetch translations |
/edge-cms/api/i18n/push |
POST | Create/update translations |
/edge-cms/api/i18n/languages |
GET | List available languages |
/edge-cms/api/blocks/import |
POST | Bulk import blocks |
- Sign in at
/edge-cms/sign-in - Navigate to
/edge-cms/i18n - Add languages and sections as needed
- Add translation keys and edit inline — changes auto-save
- Use versions to publish drafts or rollback changes
const response = await fetch('/edge-cms/public/i18n/en.json');
const translations = await response.json();Or use the SDK for type-safe access:
edgecms pull- Navigate to
/edge-cms/blocks - Create a block schema with typed properties
- Create a collection (singleton or multi-instance)
- Add block instances with content
- Publish when ready
const response = await fetch('/edge-cms/public/blocks/hero');
const { items } = await response.json();- Navigate to
/edge-cms/media - Upload files — they're automatically sanitized to kebab-case
- Organize with sections
- Reference directly in your app:
<img src="/edge-cms/public/media/my-image.jpg" alt="My Image" />- Go to
/edge-cms/settings/api-keys - Create a key with a descriptive name
- Set custom rate limits if needed
- Use the key in
EDGECMS_API_KEYfor SDK access
| Field | Description |
|---|---|
locale |
Language code (e.g., en, es) |
default |
Whether this is the fallback language |
| Field | Description |
|---|---|
name |
Section identifier for grouping content |
| Field | Description |
|---|---|
key |
Translation key |
language |
Language code |
value |
Translated text |
section |
Optional section reference |
state |
draft or live |
version |
Version number |
| Field | Description |
|---|---|
filename |
Sanitized filename |
mimeType |
File MIME type |
sizeBytes |
File size |
section |
Optional section reference |
state |
draft or live |
version |
Version number |
| Field | Description |
|---|---|
name |
Schema identifier |
type |
Property types: string, number, boolean, translation, media, block, collection |
| Field | Description |
|---|---|
name |
Collection identifier |
schema |
Associated block schema |
type |
singleton or collection |
| Field | Description |
|---|---|
collection |
Parent collection |
values |
Property values matching the schema |
state |
draft or live |
version |
Version number |
EdgeCMS is designed to run alongside your existing Cloudflare Workers app. Mount
it under /edge-cms and you're good to go — your CMS lives where your code
does, on the edge.
See LICENSE.md.
{ // D1 Database "d1_databases": [ { "binding": "DB", "database_name": "edgecms-db", "database_id": "<your-database-id>", "migrations_dir": "./migrations" } ], // KV Cache "kv_namespaces": [ { "binding": "CACHE", "id": "<your-kv-namespace-id>" } ], // R2 Storage "r2_buckets": [ { "binding": "MEDIA_BUCKET", "bucket_name": "edgecms-media" }, { "binding": "BACKUPS_BUCKET", "bucket_name": "edgecms-backups" } ], // Workflows "workflows": [ { "name": "edgecms-release-version-workflow", "binding": "RELEASE_VERSION_WORKFLOW", "class_name": "ReleaseVersionWorkflow" }, { "name": "edgecms-rollback-version-workflow", "binding": "ROLLBACK_VERSION_WORKFLOW", "class_name": "RollbackVersionWorkflow" }, { "name": "edgecms-ai-translate-workflow", "binding": "AI_TRANSLATE_WORKFLOW", "class_name": "AITranslateWorkflow" } ], // Environment "vars": { "BASE_URL": "https://your-domain.com", "TRUSTED_ORIGINS": "https://your-domain.com" } }