Koguma
A little CMS with big heart β schema-driven content management that runs entirely on Cloudflare's free tier.
Koguma gives you a headless CMS with a beautiful admin dashboard, all powered by Cloudflare Workers + D1 + R2. Define your content types in code, and Koguma handles the rest β API routes, admin UI, media storage, and a file-based content/ directory for version-controlled content.
Quickstart
1. Install
bun add koguma
npm install koguma
2. Define your content
Create a site.config.ts in your project root:
import { defineConfig, contentType, field } from 'koguma';
export default defineConfig({
siteName: 'My Site',
contentTypes: [
contentType({
id: 'post',
name: 'Blog Post',
displayField: 'title',
fields: {
title: field.text('Title').required(),
slug: field.text('Slug').required(),
body: field.markdown('Body'),
heroImage: field.image('Hero Image'),
published: field.boolean('Published').default(false),
date: field.date('Published Date')
}
})
]
});
3. Create your worker
Create a worker.ts:
import { createWorker } from 'koguma/worker';
import config from './site.config';
export default createWorker(config);
4. Configure
Create koguma.toml:
name = "my-site"
database_id = ""
That's it β two lines. Everything else is derived by convention:
- D1 database β
my-site-db
- R2 bucket β
my-site-media
- Worker name β
my-site
5. Deploy
koguma init
koguma push
Your admin dashboard is at /admin. Your API is at /api/content/:type.
Content Schema
Field Types
field.text(label) | Titles, slugs, URLs | TEXT |
field.longText(label) | Descriptions, bios | TEXT |
field.markdown(label) | Rich formatted content | TEXT (markdown string) |
field.number(label) | Counts, order | number in JSON |
field.boolean(label) | Toggles | boolean in JSON |
field.date(label) | Timestamps | string (ISO 8601) |
field.select(label, {options}) | Dropdowns | string |
field.url(label) | URLs | string |
field.email(label) | Email addresses | string |
field.phone(label) | Phone numbers | string |
field.color(label) | Hex colours | string |
field.youtube(label) | YouTube video IDs | string |
field.instagram(label) | Instagram handles | string |
field.image(label) | Image from R2 media | string (asset ID) |
field.images(label) | Array of images | string[] (asset IDs) |
field.ref(typeId, label) | Link to another entry | string (entry ID) |
field.refs(typeId, label) | Array of entry links | string[] (entry IDs) |
All fields stored inside a JSON data blob in the entries table β no per-field columns, no migrations.
Content Type Options
contentType({
id: "page",
name: "Page",
displayField: "title",
singleton: true,
fields: { ... },
});
CLI Reference
All commands auto-detect your project root by looking for koguma.toml.
koguma init | Interactive project setup β scaffold, create D1 + R2, set secret |
koguma dev | Auto-sync content/ β local D1, generate types, start dev server |
koguma push | Build + deploy + sync content to remote |
koguma pull | Download remote content + media β local content/ files |
koguma gen-types | Generate koguma.d.ts typed interfaces |
koguma tidy | Validate content/ against config, sync dirs, check fields |
Content Directory
Koguma uses a content/ directory for file-based content that lives in your git repo:
content/
βββ post/ # folder = content type ID
β βββ hello-world.md # file = one entry
β βββ our-mission.md
βββ siteSettings/
β βββ index.md # singletons use index.md
βββ media/ # optional local images
βββ hero-banner.jpg
Markdown files with frontmatter:
---
title: Our Mission
slug: our-mission
heroImage: hero-banner.jpg
published: true
date: 2026-03-01
---
## Who We Are
Rich text body, stored as markdown.
Git is your version history. No custom versioning table needed.
API Routes
Public (no auth)
GET | /api/content/:type | List all entries of a content type |
GET | /api/content/:type/:id | Get a single entry (with resolved refs) |
GET | /api/media/:key | Serve a media file from R2 |
Admin (auth required)
POST | /api/auth/login | Login with { password } |
POST | /api/auth/logout | Clear session |
GET | /api/auth/me | Check authentication |
GET | /api/admin/schema | Content type schemas (for admin UI) |
GET | /api/admin/:type | List entries |
GET | /api/admin/:type/:id | Get entry |
POST | /api/admin/:type | Create entry |
PUT | /api/admin/:type/:id | Update entry |
DELETE | /api/admin/:type/:id | Delete entry |
GET | /api/admin/media | List media assets |
POST | /api/admin/media | Upload media (multipart form) |
DELETE | /api/admin/media/:id | Delete media |
References and images are automatically resolved to nested objects in the public API (up to 2 levels deep).
Package Exports
import { defineConfig, contentType, group, field, type Infer } from 'koguma';
import { createWorker } from 'koguma/worker';
import { createClient } from 'koguma/client';
import { Markdown, useEntry, useEntries } from 'koguma/react';
import type { KogumaAsset, EntryReference } from 'koguma/types';
Local Development
echo "KOGUMA_SECRET=your-password" > .dev.vars
koguma dev
Architecture
Your Project
βββ site.config.ts β Content type definitions
βββ worker.ts β Entry point (imports koguma/worker)
βββ koguma.toml β Project config (name + database_id)
βββ .dev.vars β Local secrets (KOGUMA_SECRET)
βββ content/ β Version-controlled content files
βββ post/
β βββ hello-world.md
βββ siteSettings/
βββ index.md
Koguma (this package)
βββ src/
β βββ config/ β Schema definitions (defineConfig, field types)
β βββ api/ β Hono router (CRUD + media + auth)
β βββ db/ β D1 queries (JSON document store)
β βββ auth/ β HMAC-signed cookie sessions
β βββ media/ β R2 upload/serve/delete
β βββ admin/ β Dashboard HTML shell + JS/CSS bundle
β βββ client/ β Fetch client for consuming the API
β βββ react/ β React hooks + <Markdown> component
βββ admin/ β Vite + React admin dashboard source
βββ cli/ β CLI commands (init, dev, push, pull, gen-types, tidy)
License
MIT