rudis-cms
rudis-cms is a headless CMS that compiles Markdown/YAML documents into Cloudflare D1 (SQLite), R2 (object storage), and KV.
Features
- Schema-driven: Define your content structure in YAML, and rudis-cms generates SQL tables automatically
- Markdown with embedded images: Images in Markdown are automatically extracted, optimized, and uploaded to R2
- Multiple storage backends: Support for R2 (objects), KV (key-value), and inline storage
- Incremental updates: Only upload changed content, skip unchanged files
- Local development: Dump mode for testing with local SQLite and file storage
Architecture
┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
│ Markdown/YAML │────▶│ rudis-cms │────▶│ Cloudflare D1 │
│ Documents │ │ │ │ (SQLite) │
└─────────────────┘ │ │ └─────────────────┘
│ │ ┌─────────────────┐
│ │────▶│ Cloudflare R2 │
│ │ │ (Objects) │
└──────────────┘ └─────────────────┘
┌─────────────────┐
────▶│ Cloudflare KV │
│ (Key-Value) │
└─────────────────┘
Use Cases
- Static site generators with dynamic content
- Blog platforms with structured metadata
- Documentation sites with search functionality
- Any application needing structured content on Cloudflare’s edge
Installation
From Source
rudis-cms is written in Rust. You can build it from source:
git clone https://github.com/namachan10777/rudis-cms
cd rudis-cms
cargo install --path .
Requirements
- Rust 2024 edition (1.85+)
- Cloudflare account with:
- D1 database
- R2 bucket (optional, for object storage)
- KV namespace (optional, for key-value storage)
Environment Variables
rudis-cms requires the following environment variables for Cloudflare deployment:
| Variable | Description |
|---|---|
CF_ACCOUNT_ID | Your Cloudflare account ID |
CF_API_TOKEN | API token with D1, R2, and KV permissions |
R2_ACCESS_KEY_ID | R2 access key ID |
R2_SECRET_ACCESS_KEY | R2 secret access key |
Creating a Cloudflare API Token
- Go to Cloudflare Dashboard > My Profile > API Tokens
- Create a custom token with the following permissions:
- Account > D1 > Edit
- Account > Workers KV Storage > Edit
- Account > R2 > Edit
Getting R2 Credentials
- Go to R2 > Manage R2 API Tokens
- Create a new API token with read/write access
Quick Start
This guide will walk you through setting up a simple blog with rudis-cms.
1. Create Configuration
Create a config.yaml file:
glob: "posts/**/*.md"
name: posts
table: posts
database_id: your-d1-database-id
syntax:
type: markdown
column: body
schema:
id:
type: id
title:
type: string
required: true
date:
type: date
index: true
required: true
body:
type: markdown
required: true
storage:
type: kv
namespace: your-kv-namespace-id
image:
table: post_images
inherit_ids: [post_id]
storage:
type: r2
bucket: your-bucket-name
prefix: images
2. Create Content
Create a Markdown file at posts/hello-world.md:
---
id: hello-world
title: Hello World
date: 2024-01-01
---
This is my first post!

3. Deploy
Set your environment variables and run:
export CF_ACCOUNT_ID=your-account-id
export CF_API_TOKEN=your-api-token
export R2_ACCESS_KEY_ID=your-r2-key
export R2_SECRET_ACCESS_KEY=your-r2-secret
rudis-cms --config config.yaml batch
4. View Progress
rudis-cms shows a progress display during deployment:
📋 Loading configuration...
🔧 Compiling schema...
📄 Processing documents...
⬆️ Uploading to storage...
✅ Completed!
📊 Results:
├── ✅ posts/hello-world.md
│ ├── ⬆️ kv://namespace/hello-world
│ └── ⬆️ r2://bucket/images/image.png
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📄 Entries: 1 total
✅ Successful: 1
⬆️ Uploads: 2
⏱️ Duration: 1.23s
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Local Development
For local testing without Cloudflare, use dump mode:
rudis-cms --config config.yaml dump --storage ./storage --db ./db
This creates local SQLite databases and stores files on disk.
Configuration
rudis-cms uses a YAML configuration file to define your content structure.
Basic Structure
glob: "posts/**/*.md" # File pattern to match
name: posts # Collection name
table: posts # Main table name
database_id: xxx-xxx # Cloudflare D1 database ID
syntax:
type: markdown # or "yaml"
column: body # Column for markdown content (markdown only)
schema:
# Field definitions...
Top-Level Options
| Option | Required | Description |
|---|---|---|
glob | Yes | Glob pattern for content files |
name | Yes | Collection name |
table | Yes | Main database table name |
database_id | Yes | Cloudflare D1 database ID |
preview_database_id | No | Separate D1 database for preview |
syntax | Yes | Content format configuration |
schema | Yes | Field definitions |
Syntax Options
Markdown
syntax:
type: markdown
column: body # Field name for the markdown content
With markdown syntax, frontmatter fields are mapped to schema fields, and the body content is stored in the specified column.
YAML
syntax:
type: yaml
With YAML syntax, the entire file is parsed as YAML and mapped to schema fields.
Preview Database
You can specify a separate database for preview/draft content:
database_id: production-db-id
preview_database_id: preview-db-id
Use the --preview flag to deploy to the preview database instead.
Schema Definition
The schema defines how your content is structured and stored in the database.
Field Definition
Each field in the schema has a name and configuration:
schema:
field_name:
type: string
required: true
index: true
Common Options
| Option | Type | Default | Description |
|---|---|---|---|
type | string | - | Field type (required) |
required | bool | false | Whether the field is required |
index | bool | false | Create a database index |
Nested Records
You can define nested tables using the records type:
schema:
id:
type: id
comments:
type: records
table: post_comments
inherit_ids: [post_id]
schema:
comment_id:
type: id
text:
type: string
This creates a separate post_comments table with a foreign key relationship.
inherit_ids
The inherit_ids option specifies which parent IDs to include in the child table:
comments:
type: records
inherit_ids: [post_id] # Include post_id as foreign key
For deeply nested records, you can inherit multiple IDs:
replies:
type: records
inherit_ids: [post_id, comment_id]
Example
schema:
id:
type: id
title:
type: string
required: true
published:
type: datetime
index: true
draft:
type: boolean
index: true
body:
type: markdown
storage:
type: kv
namespace: content
tags:
type: records
table: post_tags
inherit_ids: [post_id]
schema:
tag:
type: id
Field Types
rudis-cms supports various field types for different data needs.
Basic Types
id
Primary identifier for the record.
id:
type: id
- Stored as
TEXTin SQLite - Automatically indexed
- Must be unique within the table
string
Text content.
title:
type: string
required: true
- Stored as
TEXTin SQLite
boolean
True/false values.
published:
type: boolean
index: true
- Stored as
INTEGER(0/1) in SQLite
date
Date without time.
published_date:
type: date
index: true
- Stored as
TEXTin ISO 8601 format (YYYY-MM-DD) - Indexed using
date()function
datetime
Date with time.
created_at:
type: datetime
index: true
- Stored as
TEXTin ISO 8601 format - Indexed using
datetime()function
hash
Content hash for change detection.
hash:
type: hash
- Automatically computed from file content
- Useful for cache invalidation
Content Types
markdown
Markdown content with optional image extraction.
body:
type: markdown
required: true
storage:
type: kv
namespace: content-namespace
image:
table: post_images
inherit_ids: [post_id]
embed_svg_threshold: 8192
storage:
type: r2
bucket: my-bucket
prefix: images
config: {}
Options:
storage: Where to store the compiled markdownimage: Configuration for extracted imagesimage.embed_svg_threshold: SVG files smaller than this (bytes) are embedded inlineconfig: Additional markdown processing options
image
Single image field.
og_image:
type: image
storage:
type: r2
bucket: my-bucket
prefix: og-images
file
Generic file attachment.
attachment:
type: file
storage:
type: r2
bucket: my-bucket
prefix: attachments
Relational Types
records
Nested table with multiple records.
tags:
type: records
table: post_tags
inherit_ids: [post_id]
schema:
tag:
type: id
Creates a separate table with foreign key relationship.
Storage Options
rudis-cms supports multiple storage backends for different content types.
R2 (Object Storage)
Cloudflare R2 for binary files like images.
storage:
type: r2
bucket: my-bucket
prefix: images/posts
| Option | Required | Description |
|---|---|---|
bucket | Yes | R2 bucket name |
prefix | No | Key prefix for objects |
Objects are stored with content-addressed keys based on their hash, ensuring deduplication.
KV (Key-Value)
Cloudflare KV for text content like compiled markdown.
storage:
type: kv
namespace: content-namespace-id
| Option | Required | Description |
|---|---|---|
namespace | Yes | KV namespace ID |
Inline
Store content directly in the database.
storage:
type: inline
Best for small content that doesn’t need separate storage.
Asset
For static assets served directly.
storage:
type: asset
prefix: static
Storage Pointer Format
In the database, storage references are stored as JSON with pointer information:
{
"hash": "abc123...",
"pointer": "r2://bucket/prefix/key",
"content_type": "image/png",
"size": 12345
}
The pointer format indicates the storage type:
r2://bucket/key- R2 objectkv://namespace/key- KV entryasset://path- Asset file
Deduplication
rudis-cms uses content hashing (BLAKE3) to deduplicate uploads:
- Before upload, the file hash is computed
- If an object with the same hash exists, upload is skipped
- Progress shows
⬆️for new uploads and⏭️for skipped
Use the -f / --force flag to force re-upload of all objects.
CLI Commands
Global Options
rudis-cms --config <CONFIG> <COMMAND>
| Option | Short | Description |
|---|---|---|
--config | -c | Path to configuration file (required) |
Commands
batch
Deploy content to Cloudflare.
rudis-cms -c config.yaml batch [OPTIONS]
| Option | Short | Description |
|---|---|---|
--force | -f | Force re-upload all objects |
--preview | -p | Deploy to preview database |
Example:
# Normal deployment
rudis-cms -c config.yaml batch
# Force re-upload everything
rudis-cms -c config.yaml batch -f
# Deploy to preview database
rudis-cms -c config.yaml batch --preview
dump
Export to local files for development/testing.
rudis-cms -c config.yaml dump --storage <PATH> --db <PATH>
| Option | Description |
|---|---|
--storage | Directory for storage files |
--db | Directory for SQLite database |
Example:
rudis-cms -c config.yaml dump --storage ./local-storage --db ./local-db
show-schema
Display generated schemas.
rudis-cms -c config.yaml show-schema <SUBCOMMAND>
show-schema sql
Show SQL DDL statements.
rudis-cms -c config.yaml show-schema sql [OPTIONS]
| Option | Description |
|---|---|
--fetch-objects | Include fetch objects query |
show-schema typescript
Generate TypeScript type definitions.
rudis-cms -c config.yaml show-schema typescript [OPTIONS]
| Option | Description |
|---|---|
--save <DIR> | Save to directory |
--valibot | Generate Valibot schemas |
Example:
# Print to stdout
rudis-cms -c config.yaml show-schema typescript
# Save with Valibot validation
rudis-cms -c config.yaml show-schema typescript --save ./generated --valibot
Exit Codes
| Code | Description |
|---|---|
| 0 | Success |
| 1 | Error |
Environment Variables
See Installation for required environment variables.
SQL Schema
rudis-cms automatically generates SQL tables based on your schema definition.
Generated Tables
For a schema like:
table: posts
schema:
id:
type: id
title:
type: string
required: true
date:
type: date
index: true
body:
type: markdown
storage:
type: kv
namespace: content
tags:
type: records
table: post_tags
inherit_ids: [post_id]
schema:
tag:
type: id
The following SQL is generated:
CREATE TABLE IF NOT EXISTS posts (
id TEXT NOT NULL,
title TEXT NOT NULL,
date TEXT NOT NULL,
body TEXT NOT NULL,
PRIMARY KEY (id)
);
CREATE INDEX IF NOT EXISTS index_posts_id ON posts(id);
CREATE INDEX IF NOT EXISTS index_posts_date ON posts(date(date));
CREATE TABLE IF NOT EXISTS post_tags (
post_id TEXT NOT NULL,
tag TEXT NOT NULL,
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag)
);
CREATE INDEX IF NOT EXISTS index_post_tags_tag ON post_tags(tag);
Type Mappings
| rudis-cms Type | SQLite Type | Notes |
|---|---|---|
id | TEXT NOT NULL | Primary key |
string | TEXT | NOT NULL if required |
boolean | INTEGER | 0 or 1 |
date | TEXT | ISO 8601 format |
datetime | TEXT | ISO 8601 format |
hash | TEXT | BLAKE3 hash |
markdown | TEXT | JSON with storage pointer |
image | TEXT | JSON with storage pointer |
file | TEXT | JSON with storage pointer |
Storage Pointers
Content fields (markdown, image, file) are stored as JSON:
{
"hash": "abc123def456...",
"size": 12345,
"content_type": "text/markdown",
"pointer": "kv://namespace-id/key"
}
Foreign Keys
Child tables created by records type include:
- Foreign key constraint with
ON DELETE CASCADE - Composite primary key including parent IDs
Viewing Generated SQL
Use the CLI to view generated SQL:
rudis-cms -c config.yaml show-schema sql