A headless CMS written in Rust. Define your schema in Lua, extend everything with hooks, query via gRPC, manage content through an HTMX admin UI. Single binary, SQLite, zero infrastructure.
Alpha software. While in
0.x, breaking changes may appear without prior notice.
docker run -p 3000:3000 -p 50051:50051 ghcr.io/dkluhzeb/crap-cms:latest serve -C /exampleOpen http://localhost:3000/admin — login: admin@crap.studio / admin123
Or install the latest release with the shell installer (Linux x86_64 / ARM64):
curl -fsSL https://raw.githubusercontent.com/dkluhzeb/crap-cms/main/scripts/install.sh | bashThe installer downloads the matching binary, verifies it against SHA256SUMS, and places it under ~/.local/share/crap-cms/versions/<version>/. It wires up a shim at ~/.local/bin/crap-cms — add that directory to your PATH if it isn't already.
Inspect the script before running (recommended)
curl -fsSL https://raw.githubusercontent.com/dkluhzeb/crap-cms/main/scripts/install.sh -o install.sh
less install.sh # review
sha256sum install.sh # compare against the repo's scripts/install.sh
bash install.sh # run once you're satisfiedFor a reproducible install, pin the URL to a tag: …/crap-cms/v0.1.0-alpha.5/scripts/install.sh.
Prefer a direct release download? Grab crap-cms-linux-x86_64 (or the arch you need), chmod +x, and run — no other dependencies.
Once installed, the binary manages itself:
crap-cms update check # is a newer release out?
crap-cms update list # remote releases, marked with (installed) and *
crap-cms update install v0.1.0-alpha.5
crap-cms update use v0.1.0-alpha.5
crap-cms update # shortcut: install latest + switch to itcrap-cms serve prints a one-line nudge on startup when a newer release is cached (24h TTL, populated by crap-cms update check). Set [update] check_on_startup = false in crap.toml to silence it.
- Collections with 20 field types (text, number, textarea, richtext, select, radio, checkbox, date, email, json, code, relationship, upload, array, group, blocks, row, collapsible, tabs, join)
- Globals — single-document collections for site-wide settings
- Lua hooks at three levels (field, collection, global) with full CRUD access inside transactions
- Access control — collection-level and field-level, with filter constraints
- Authentication — JWT sessions, password login, custom auth strategies, email verification, password reset
- Uploads — file uploads with automatic image resizing and format conversion (WebP, AVIF)
- Relationships — has-one and has-many with configurable population depth and caching
- Localization — per-field opt-in with locale-suffixed columns and fallback
- Versions & Drafts — document version history with draft/publish workflow
- Live updates — real-time mutation events via SSE and gRPC streaming
- Background jobs — cron scheduling, retries, queues, heartbeat monitoring
- Admin UI — template overlay system, theme switching, Web Components, fully overridable
- gRPC API — full CRUD with filtering, pagination, server reflection. REST proxy available
- MCP server — Model Context Protocol integration for AI tooling
- File logging — optional rotating file logs with
crap-cms logsviewer, auto-enabled for detached mode - CLI — interactive scaffolding, blueprints, data export/import, backups
For full documentation, see the user manual.
I built several Rust/WebAssembly frontend projects and couldn't find a CMS that fit the stack. So I built one.
The idea: a simple CMS written in Rust, extensible via Lua, with no complicated build steps or infrastructure requirements.
Inspiration came from what I consider the best solutions out there:
- Lua scripting — modeled after Neovim and Awesome WM
- Hook system — inspired by Payload CMS, an excellent CMS for anyone needing a production-ready solution
- CLI tooling — influenced by Laravel's Artisan
- SQLite + WAL + FTS — single binary, zero external dependencies, database layer abstracted behind a trait
- HTMX + Web Components — themeable like WordPress child themes, no frontend build step
- gRPC API — binary protocol with streaming, plus a REST proxy for plain JSON over HTTP
Production — mount your own config directory:
docker run -v /path/to/config:/config -p 3000:3000 -p 50051:50051 \
ghcr.io/dkluhzeb/crap-cms:latestImages are Alpine-based (~30 MB) and published to ghcr.io/dkluhzeb/crap-cms.
| Tag | Description |
|---|---|
latest |
Most recent tagged release |
X.Y.Z-alpha.N |
Tagged release |
X.Y |
Latest patch in a minor series |
nightly |
Latest main build (x86_64) |
sha-<commit> |
Pinned to a specific commit |
Pre-built static binaries are attached to each GitHub Release:
crap-cms-linux-x86_64— Linux x86_64 (musl, fully static)crap-cms-linux-aarch64— Linux ARM64 (musl, fully static)crap-cms-windows-x86_64.exe— Windows x86_64
No runtime dependencies required. An example.tar.gz with a sample project is included in each release.
| Component | Technology |
|---|---|
| Language | Rust (edition 2024) |
| Web / Admin | Axum + Handlebars + HTMX |
| API | gRPC via Tonic + Prost |
| Database | SQLite via rusqlite (WAL mode) |
| Hooks | Lua 5.4 via mlua |
| IDs | nanoid |
The repo is a Cargo workspace with four members. Each non-trivial member ships its own README — those are the source of truth for module-level conventions, derive surfaces, and authoring patterns. This README only points at them.
| Member | Path | What it does | Details |
|---|---|---|---|
crap-cms |
. |
Main binary + library (admin UI, gRPC API, Lua hooks, scheduler, MCP) | source tree below |
crap-cms-macros |
macros/ |
Proc-macro derives that drive typegen (LuaAnnotation, LuaAlias, #[lua_fn], …) |
macros/README.md |
crap-cms-e2e |
e2e/ |
End-to-end + HTML integration tests (headless Chrome via chromiumoxide) |
e2e/src/{browser,helpers,html}.rs + e2e/tests/*.rs |
xtask |
xtask/ |
Workspace task runner — typegen + reference-doc regeneration with CI drift gates | xtask/README.md |
default-members = [".", "macros"] — plain cargo build / cargo test from the root skips the e2e and xtask crates. Workspace-wide deps + lints are inherited via [workspace.package], [workspace.dependencies], and [workspace.lints] in the root Cargo.toml; members opt in with *.workspace = true.
src/
├── main.rs # binary entry point, subcommand dispatch
├── lib.rs # crate exports (incl. pub `typegen` + `docgen` for xtask)
├── config/ # crap.toml loading + defaults
├── core/ # collection, field, document types
├── db/ # pool, migrations, query builder
├── service/ # service layer — chokepoint for CRUD lifecycle
├── hooks/ # Lua VM, crap.* API, hook lifecycle
├── admin/ # Axum admin UI (handlers, templates, typed page contexts)
├── api/ # Tonic gRPC service
├── scheduler/ # background job scheduler
├── mcp/ # Model Context Protocol server
├── cli/ # CLI argument parsing
├── commands/ # CLI subcommand implementations
├── typegen/ # type generation (Rust, Lua, TS, Python, Go) — derive consumers
└── scaffold/ # init/make scaffolding
The Lua-API surface (crap.collections, crap.hooks, crap.config, …) lives under src/hooks/lua_api/ — each #[lua_fn] annotated function there contributes one block to the generated types/crap.lua. See macros/README.md for the derive contract.
git config core.hooksPath .githooks # enable shared git hooks (fmt + clippy pre-commit)
cargo build # compile (default-members: main + macros)
cargo test --workspace --exclude crap-cms-e2e # what CI's `check` job runs
cargo test -p crap-cms-e2e -- --test-threads=1 # browser/HTML integration tests (needs Chrome)
cargo tarpaulin --out html # coverage report
crap-cms serve -C ./example # run with example configAfter touching anything that affects the generated types/crap.lua (any #[derive(Lua*)], #[lua_fn], or lua_table! input) or the typed admin-page contexts under src/admin/context/page/, regenerate via the workspace task runner:
cargo xtask gen-lua-types # regen types/crap.lua
cargo xtask gen-template-doc # regen docs/src/admin-ui/reference/template-context.mdCI gates on --check variants of both. See xtask/README.md for the full subcommand list and how to add a new one.
Default templates and static files are compiled into the binary via include_dir!. The config directory overlay takes priority — any file placed in {config_dir}/static/ or {config_dir}/templates/ is served from disk without rebuilding. Only changes to the embedded defaults (under static/ or templates/ in the source tree) require cargo build.
Dev mode (admin.dev_mode = true in crap.toml) reloads templates from disk on every request instead of caching them.
The admin UI ships a strict, nonce-based Content-Security-Policy: inline scripts are only allowed when they carry a per-request nonce. If your overlay template emits an inline <script>, it must use:
<script nonce="{{crap.csp_nonce}}">
/* your code */
</script>The nonce is regenerated on every request and exposed to all admin templates as crap.csp_nonce (alongside crap.version, crap.build_hash, etc.). Inline event handler attributes (onclick="...", onchange="...", …) are blocked under the default policy — replace them with delegated listeners (see static/components/password-toggle.js for a minimal pattern) or with a small inline <script nonce="..."> block. Inline style="..." attributes are still permitted (style-src keeps 'unsafe-inline' for now). CSP can be tuned or disabled via [admin.csp] in crap.toml.
Requires grpcurl and a running server:
source tests/api.sh
find_posts
create_postRequires ghz, grpcurl, protoc, jq, and a running server:
./tests/grpc_loadtest.sh # all scenarios, default settings
./tests/grpc_loadtest.sh --duration 5 # shorter runs
./tests/grpc_loadtest.sh --concurrency 1,10 # custom concurrency levels
./tests/grpc_loadtest.sh --scenarios find,count # specific scenarios onlyScenarios: describe, count, find, find_where, find_by_id, find_deep, create, update.
cd docs && mdbook serve # local preview at localhost:3000| Workflow | Trigger | What it does |
|---|---|---|
| CI | Every push & PR | fmt, clippy, tests, cargo xtask gen-lua-types --check, cargo xtask gen-template-doc --check, security audit |
| Nightly | Push to main | x86_64 musl binary, Docker nightly tag, docs deploy |
| Release | Tag v* |
Multi-arch binaries, Docker semver tags, GitHub Release, docs deploy |
| Feature | Default | Description |
|---|---|---|
sqlite |
yes | SQLite backend (bundled, no runtime dependency). |
postgres |
no | PostgreSQL backend (via tokio-postgres + deadpool-postgres). |
s3-storage |
no | S3-compatible upload storage (AWS S3, MinIO, R2, B2, Spaces). |
redis |
no | Redis-backed cache and cross-node live-update transport. |
browser-tests |
no | Headless Chrome end-to-end tests (via chromiumoxide). |
Enable a feature at build time with cargo build --features <name> (combine with commas).
MIT