A 1:1, offline-first clone of Strapi built entirely in Rust.
ferriscms reproduces Strapi's core headless-CMS workflow with a single Rust codebase that runs in two modes:
- Offline desktop β an embedded SQLite database, no server, no config.
- Online server β an Axum server (SQLite by default, PostgreSQL when
DATABASE_URLsays so) serving a Strapi-compatible REST API plus the Dioxus admin UI.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β shared library crates β
β core-domain, core-schema, db, dynamic-store, services, β
β api-types, client-core β
ββββββββββββββββ²βββββββββββββββββββββββββββββββ²ββββββββββββββββ
β in-process calls β HTTP
βββββββββββββ΄βββββββββββ βββββββββββββ΄ββββββββββββββ
β desktop-bin (OFFLINE)β β server-bin (ONLINE) β
β Dioxus UI β client-core β Axum β services β
β services + SQLite (embedded)β Postgres / SQLite β
ββββββββββββββββββββββββββ βββββββββββββ¬ββββββββββββββ
β HTTP
βββββββββββββ΄ββββββββββββββ
β Web admin (Dioxus WASM) β
β client-core (HTTP) β
βββββββββββββββββββββββββββ
The project implements the "core" of Strapi (no code generation, plugin system, or GraphQL yet):
- Content-Type Builder β visually define collection types, single types,
components, and dynamic zones at runtime. Field types include text, rich
text (Markdown/Blocks), number, date, boolean, email, password, enum,
JSON, UID, media, relation, component, and dynamic zone. All six relation
kinds,
New / Modified / Deletedstaging badges, undo/redo/discard. - Content Manager β schema-driven entry forms, list/edit views, filters, sorting, pagination, Draft & Publish, and discard-draft controls.
- Saved Content Manager views β multiple Grid, Kanban, Gallery, and Calendar presentations per content type, with persisted filters, sorts, columns, defaults, duplication, ordering, and server-side record queries.
- Dynamic schema β real tables β schemas are stored as JSON and applied to the database at runtime (SeaQuery-generated DDL). No manual SQL.
- Strapi-compatible REST API β
filters,populate,sort,pagination,fields,locale, andstatusquery params, with Strapi's{ data, meta }envelope shape. - Media Library β upload files, list/delete, served from
/uploads. - RBAC β roles and a permission matrix (Super Admin, Editor, Author seeded).
- i18n β locale registry and localized content variants.
- API Tokens β read-only/full/custom tokens for the public API.
- JWT admin auth β first-run registration + login (argon2 password hashing).
| Layer | Technology |
|---|---|
| Language | Rust (edition 2021) |
| Web server | Axum 0.8 |
| ORM / query builder | SeaORM 2.0 + SeaQuery 1.0 |
| Migrations | sea-orm-migration (system) + runtime DDL (user content-types) |
| Database | SQLite (default, embedded) / PostgreSQL 14+ (DATABASE_URL, online) |
| UI | Dioxus 0.7 (native desktop + WASM web) |
| Async runtime | Tokio |
| Auth | JWT (jsonwebtoken) + argon2 password hashing |
| Serialization | serde / serde_json |
A Cargo workspace of library crates plus two thin binary crates:
ferriscms/
ββ Cargo.toml # workspace manifest + shared dependencies
ββ docs/ # planning / design docs (see below)
ββ crates/
β ββ core-domain/ # pure domain types (no IO): kinds, fields, relations, UID
β ββ core-schema/ # schema model, JSON (de)serialization, validation, diffing
β ββ api-types/ # shared request/response DTOs + Strapi query parser
β ββ db/ # SeaORM 2.0 system entities, migrations, seeding
β ββ dynamic-store/ # runtime DDL + CRUD for user-defined content-types
β ββ services/ # business logic (CTB, content, media, rbac, i18n, auth)
β ββ api-rest/ # Axum routers: /api, /admin, /content-type-builder
β ββ client-core/ # transport-abstract client the UI calls
β ββ ui/ # design system: tokens, widgets, screens (framework-agnostic)
β ββ app/ # Dioxus admin UI (web + desktop), calls client-core
β ββ server-bin/ # binary: online Axum server (ferriscms)
β ββ desktop-bin/ # binary: offline desktop + embedded server (ferriscms-desktop)
- Rust β the workspace targets Rust edition 2021; the docs assume MSRV 1.82+. Installed with rustup.
- Dioxus CLI (
dx) β only needed for the web/desktop UI build:cargo install dioxus-cli
- PostgreSQL β optional. The webserver (
server-bin) defaults to a local SQLite file (ferriscms.db); setDATABASE_URLto apostgres://β¦URL to run it on PostgreSQL instead. The desktop app always uses embedded SQLite.
Build the entire workspace (backend + UI libraries):
cargo build --workspaceBuild just the two runtime binaries:
cargo build -p server-bin -p desktop-binBuild the Dioxus admin UI (the ferriscms app crate). The dx CLI needs to
locate the app's Dioxus.toml, which lives in crates/app/. Either run from
inside that directory, or point dx at the package with --package ferriscms:
# native desktop app
cd crates/app
dx build --desktop --features desktop
# web (WASM) bundle -> crates/app/target/dx/ferriscms/debug/web
cd crates/app
dx build --web
# ...or, from the workspace root, use the package flag:
dx build --web --package ferriscmscargo test --workspaceTests include a full end-to-end admin workflow against the Axum router
in-memory (crates/api-rest/tests/auth_workflow.rs) and dynamic-store CRUD
integration tests.
Views are scoped to a content type and never copy records. The admin API is:
GET /admin/content-manager/content-types/:uid/views
POST /admin/content-manager/content-types/:uid/views
GET /admin/content-manager/content-types/:uid/views/:id
PUT /admin/content-manager/content-types/:uid/views/:id
DELETE /admin/content-manager/content-types/:uid/views/:id
POST /admin/content-manager/content-types/:uid/views/:id/duplicate
POST /admin/content-manager/content-types/:uid/views/:id/default
POST /admin/content-manager/content-types/:uid/views/reorder
GET /admin/content-manager/content-types/:uid/views/:id/records
The records endpoint translates the saved typed filter/sort configuration through the existing dynamic-store query builder, so filtering, sorting, and pagination remain server-side and content-type RBAC still applies.
The server loads config/{RUST_ENV}.toml through axum-conf; RUST_ENV
defaults to prod. BIND_ADDR still overrides the configured bind address,
and database, JWT, and media settings remain available through their existing
environment variables.
JSON imports are controlled under [import.json]:
[import.json]
enabled = true
max_file_bytes = 10485760
default_dataset = "data"JSON imports are enabled by default, limited to 10 MiB, and support either a
top-level array or an object containing named array datasets. The import
request's dataset selects a named dataset; default_dataset is used when it
is omitted or empty.
Starts the Axum REST API + admin API on the configured port, running
migrations and seeding roles/locales on boot. With no configuration it uses a
local SQLite file (ferriscms.db in the working directory, gitignored), so
cargo run -p server-bin works out of the box; set DATABASE_URL to a
postgres://β¦ URL to use PostgreSQL.
cargo run -p server-bin| Env var | Default | Purpose |
|---|---|---|
DATABASE_URL |
sqlite://ferriscms.db?mode=rwc |
Local SQLite file by default; set a postgres://β¦ URL for PostgreSQL |
BIND_ADDR |
0.0.0.0:8080 |
HTTP listen address |
JWT_SECRET |
change-me-in-production |
HS256 signing secret (set in production!) |
MEDIA_STORAGE_DIR |
media |
Directory for uploaded files |
On first boot, register the first admin via POST /admin/register-admin
(or use the UI). Then log in through POST /admin/login to get a JWT.
Run the backend and the web UI together with cargo-make. The --no-workspace
flag prevents cargo-make from trying to run the task in every workspace crate.
The backend listens on port 8080; the Dioxus web server uses port 8081.
In development mode, the migration creates admin / admin as a local
Super Admin, and the server imports the ERP content types and sample data from
examples/content-types/. The import runs in upsert mode, so restarting the
development server does not duplicate the examples.
Install cargo-make if it is not already available:
cargo install cargo-makecargo make --no-workspace devWithout cargo-make, use two terminals from the repository root:
# Terminal 1: Axum backend
FERRISCMS_ENV=development cargo runThe development bootstrap is configured in config/dev.toml under
[dev.bootstrap]. Set enabled = false there to run the development server
without importing the ERP examples.
# Terminal 2: Dioxus web UI
cd crates/app
FERRISCMS_API_URL=http://127.0.0.1:8080 dx serve --web --package ferriscms --port 8081The dx command requires the Dioxus CLI version matching the app dependency
(currently 0.7.10). The web UI is available at http://127.0.0.1:8081 and
the backend at http://127.0.0.1:8080.
Runs the full backend (SQLite + migrations + seed) and exposes the REST API on a local HTTP port β no external database or server required.
cargo run -p desktop-bin| Env var | Default | Purpose |
|---|---|---|
STRAPI_DB_PATH |
ferriscms-desktop.db |
Path to the local SQLite file |
STRAPI_BIND_ADDR |
127.0.0.1:1338 |
Local HTTP listen address |
JWT_SECRET |
desktop-local-dev |
Signing secret |
MEDIA_STORAGE_DIR |
media |
Upload directory |
The Dioxus admin UI (the ferriscms app crate) talks to the backend through
client-core. On web it uses same-origin requests; on desktop/native
it targets FERRISCMS_API_URL (default http://127.0.0.1:1337).
Because the app's Dioxus.toml lives in crates/app/, run dx from inside
that directory and select the app package explicitly:
# Dev server (hot reload) for the web UI
cd crates/app
FERRISCMS_API_URL=http://127.0.0.1:8080 dx serve --web --package ferriscms --port 8081
# Native desktop app
cd crates/app
dx run --desktop --features desktop
# or, from the workspace root:
dx run --desktop --package ferriscms --features desktop
# Point a desktop/native build at a running server
FERRISCMS_API_URL=http://127.0.0.1:1337 ./target/debug/ferriscmsThe web build is a separate static bundle. Run it alongside the server (
cargo run -p server-bin) so the UI can reach the API, or host thecrates/app/target/dx/ferriscms/debug/weboutput on any static server that proxies/api,/admin, and/content-type-builderto the Axum process.
The repo ships a Dockerfile for the server binary and a Helm chart so the
CMS can be deployed to Kubernetes.
Build the ferriscms image locally. The build produces a single
self-contained binary that embeds both the Axum webserver and the Dioxus WASM
admin UI (via rust-embed):
docker build -t ferriscms .The image is the webserver and uses PostgreSQL (external). The one binary
serves the admin UI at / and the REST API at /api, /admin, and
/content-type-builder. Media is stored in /data/media. Set the DB
connection at run time:
docker run --rm -p 1337:1337 \
-e DATABASE_URL='postgres://user:pass@host:5432/ferriscms' \
-e JWT_SECRET='a-strong-secret' \
-e MEDIA_STORAGE_DIR=/data/media \
-v ferriscms-media:/data/media \
ferriscmsFor development, you can serve the UI from a directory instead of the embedded
copy by setting FERRISCMS_UI_DIR (e.g. point it at a dx serve output).
The chart lives in deploy/helm/ferriscms/:
helm lint deploy/helm/ferriscms
helm template ferriscms deploy/helm/ferriscms
helm install ferriscms deploy/helm/ferriscms \
--set image.tag=0.2.0 \
--set env.JWT_SECRET='a-strong-secret'Useful --set overrides:
| Value | Default | Purpose |
|---|---|---|
image.tag |
appVersion |
Image tag to deploy |
env.DATABASE_URL |
postgres://postgres:postgres@postgres:5432/ferriscms |
PostgreSQL connection URL |
env.JWT_SECRET |
change-me-in-production |
Signing secret β set in production! |
persistence.enabled |
true |
Mount a PVC at /data for media |
persistence.size |
1Gi |
PVC size |
A docker-compose.yml starts a PostgreSQL database plus
the webserver, using profiles:
# Build the image from the local Dockerfile (dev)
docker compose --profile local up --build
# Pull and run the prebuilt image from GHCR (registry)
docker compose --profile registry upBoth profiles share the same Postgres database and expose ferriscms on
localhost:1337. Set JWT_SECRET (and optionally POSTGRES_USER,
POSTGRES_PASSWORD, POSTGRES_DB) via a .env file. For the registry
profile, override the image with FERRISCMS_IMAGE=ghcr.io/dinosath/ferris-cms:<tag>.