A lightweight, personal content management system built on Cloudflare's edge platform. Designed for managing content across your personal projects, portfolios, and websites.
- Edge-Native: Runs entirely on Cloudflare Workers, D1, and R2
- Polymorphic Content: Flexible content modeling with JSON storage
- Media Management: Full-featured media library with R2 storage
- Version Control: Track content history and restore previous versions
- Webhooks: Event-driven integrations with external services
- API Keys: Programmatic access for headless CMS use cases
- Audit Logging: Complete audit trail of all changes
- Globals: Singleton documents for site-wide content
- Multi-language: Locale support for internationalization
- API: Hono-based REST API running on Cloudflare Workers
- Database: D1 (SQLite) for structured data
- Storage: R2 for media files
- Auth: JWT-based authentication with secure sessions
- Packages:
@cloudcms/api- Main API worker@cloudcms/auth- Authentication utilities@cloudcms/db- Database layer with Drizzle ORM@cloudcms/types- Shared TypeScript types
- Node.js 20+
- pnpm 9+
- Cloudflare account
- Wrangler CLI
- Install dependencies:
pnpm install- Create your D1 database:
# For local development
wrangler d1 create cloudcms-db --local
# For production
wrangler d1 create cloudcms-db
# Note the database_id from the output- Update
apps/api/wrangler.jsoncwith your production database ID:
- Create your R2 bucket:
# For production
wrangler r2 bucket create cloudcms-media- Run migrations:
# Local development
pnpm db:migrate
# Production
pnpm db:migrate:prod- Set production secrets:
wrangler secret put JWT_SECRET --env production
# Enter a strong random string (e.g., openssl rand -base64 32)
wrangler secret put COOKIE_DOMAIN --env production
# Enter your domain (e.g., yourdomain.com)Start the development server:
pnpm dev:apiThe API will be available at http://localhost:8787
On first run, create your admin account:
curl -X POST http://localhost:8787/api/setup \
-H "Content-Type: application/json" \
-d '{
"email": "admin@example.com",
"password": "your-secure-password",
"name": "Admin User"
}'Deploy to Cloudflare:
# Deploy to production
cd apps/api
pnpm deployLogin
POST /api/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "password"
}
Get Current User
GET /api/auth/me
Cookie: auth_token=<token>
List Entries
GET /api/collections/:slug?page=1&limit=10&status=published
Create Entry
POST /api/collections/:slug
Content-Type: application/json
{
"title": "My Post",
"content": "Post content...",
"_status": "published"
}
Update Entry
PATCH /api/collections/:slug/:id
Content-Type: application/json
{
"title": "Updated Title"
}
Delete Entry
DELETE /api/collections/:slug/:id?hard=false
Upload Media
POST /api/media/upload
Content-Type: multipart/form-data
file: <file>
altText: "Image description"
folderId: "optional-folder-id"
List Media
GET /api/media?page=1&limit=20&folderId=<id>&mimeType=image/jpeg
Get Global
GET /api/globals/:slug?locale=en
Update Global
PATCH /api/globals/:slug?locale=en
Content-Type: application/json
{
"siteName": "My Site",
"description": "Site description"
}
List Users
GET /api/admin/users
Create API Key
POST /api/admin/api-keys
Content-Type: application/json
{
"name": "My API Key",
"scopes": ["*"],
"expiresAt": "2025-12-31T23:59:59Z"
}
Manage Settings
GET /api/admin/settings
PUT /api/admin/settings/:key
Create Webhook
POST /api/webhooks
Content-Type: application/json
{
"name": "My Webhook",
"url": "https://example.com/webhook",
"events": ["entry.create", "entry.update"],
"collections": ["posts"]
}
The CMS uses a polymorphic content model where all collection entries are stored in a single entries table with JSON data. This provides maximum flexibility without requiring schema migrations for content changes.
- users - User accounts and authentication
- sessions - Active user sessions
- api_keys - API authentication tokens
- entries - All collection content (polymorphic)
- entry_versions - Version history
- global_values - Singleton documents
- media - Media metadata
- folders - Media organization
- webhooks - Webhook configurations
- audit_logs - Complete audit trail
- settings - System configuration
pnpm dev- Start all development serverspnpm dev:api- Start API worker onlypnpm build- Build all packagespnpm typecheck- Type check all packagespnpm db:migrate- Run migrations (local)pnpm db:migrate:prod- Run migrations (production)pnpm db:studio- Open Drizzle Studio
- Passwords are hashed with PBKDF2 (600,000 iterations)
- JWTs are signed with HS256
- Sessions are stored in HTTP-only cookies
- API keys are hashed with SHA-256
- Webhook payloads are signed with HMAC-SHA256
- Rate limiting via Cloudflare's built-in protection
- Audit logging for all modifications
- Global Edge Network: Low latency worldwide
- Serverless: No infrastructure to manage
- Cost Effective: Generous free tier, pay-per-use
- Integrated Stack: D1, R2, KV all in one platform
- Flexibility: Add fields without migrations
- Simplicity: One table for all content
- Performance: Optimized for read-heavy workloads
- Type Safety: TypeScript types at application level
This is designed as a personal CMS. If you need multi-tenancy, consider:
- Running multiple workers (one per project)
- Using namespace prefixes in collection slugs
- Forking and extending the codebase
- Admin UI (Next.js)
- Collection config management
- Field validation
- Rich text editor
- Image transformations
- Search/filtering
- Scheduled publishing
- Export/import tools
- CLI tools
This is a personal project, but suggestions and PRs are welcome!
MIT