Skip to main content

Model the domain.
Ship it in Rust.

Riverbase is a Rust framework for domain-driven HTTP backends. Declare commands, aggregates and queries; get typed routes, OpenAPI, OIDC auth, Casbin policy, tenant-safe reads, and an audit trail that commits with every change.

cargo add riverbase_core riverbase_proc riverbase_http --git https://github.com/riverbase-io/riverbase
Read the quick startBrowse the docs

Rust 1.78+axum · Diesel · Postgres

audit log · riverbase.todoappend-only
  1. 001030todo.updatedupdate-todotodo:2d9e0a47
  2. 001029todo.clearedclear-donetodo:*
  3. 001028todo.createdcreate-todotodo:2d9e0a47
  4. 001027todo.updatedupdate-todotodo:c04e5b18
  5. 001026todo.createdcreate-todotodo:c04e5b18
  6. 001025todo.updatedupdate-todotodo:7f3a91c2
  7. 001024todo.createdcreate-todotodo:7f3a91c2

Built on the Rust ecosystem you already trust

  • axum
  • tokio
  • Diesel
  • diesel-async
  • PostgreSQL
  • serde
  • OpenAPI
  • JSON Schema
  • OpenID Connect
  • Keycloak
  • Casbin
  • ractor
  • NATS
  • Redis
  • OpenDAL
  • RxDB
  • SSE
  • WebSocket

riverbase_http::RiverbaseApp

Mount what you need, nothing more

Every route exists because a line in main.rs put it there. Switch a line off to see what the service stops serving.

Read the docs
service/src/main.rs
app.serve().await?;
The service serves
  • GET/api/openapi.jsonOpenAPI document
  • POST/api/riverbase.todo/create-todo:post/todoCommand
  • POST/api/riverbase.todo/update-todo:exec/todo/{id}Command
  • GET/api/riverbase.todo/todo.listQuery: filter, sort, page
  • GET/api/riverbase.todo/todo.item/{id}Query: one row
  • GET/api/auth/infoWho is calling: OIDC, sessions, profiles
  • POST/api/auth/switch-profile/{id}Switch the active profile

7 routes, each behind auth and Casbin unless declared public

riverbase_core · riverbase_proc · riverbase_http

Build a domain in six files

The todo reference service from the tutorial, in the order you would write it.

Read the docs
domain/src/postgres/schema.rs
use riverbase_core::diesel_table_with_domain_fields;
diesel_table_with_domain_fields! {
todo_examples.todo_items (_id) {
title -> Text,
done -> Bool,
notes -> Text,
version -> Int4,
}
}

riverbase_core::command

What happens to a command

One request, one transaction. The state change and its audit trail commit together, then integration messages leave through the outbox. Scrub the history of one row below.

Read the docs
  1. Routecreate-todo:​postaxum decodes {"payload": …} into CreateTodoPayload. Bad input stops here.
  2. Policycreate-todo.​executeThe caller is authenticated, then Casbin checks the activity. Empty policy denies.
  3. EngineCommandEngineA ractor actor opens one Postgres transaction for the whole command.
  4. AggregateTodoAggregate::​create#[domain_action] writes the row and names the event. Writes outside it fail.
  5. Audittodo.​createdEvent, activity, response and outbox rows commit with the state, or not at all.
  6. Readtodo.​listThe query engine serves the row, filtered by tenant and scope policy.
History of todo:7f3a91c2seq 001036 · 4 of 4 events
_id
7f3a91c2
title
null
done
null
notes
null
_etag
null
_updated
10:15:40

State is stored, not replayed. Each audited event records the row it produced, so the history shows exactly what changed and when.

What ships in the box

The parts most backends rebuild by hand, already wired to the same domain.

Crates and features

Typed end to end

Payloads
Rust structs generate the decoder, JSON Schema and OpenAPI from one declaration
riverbase_core
SQL
Diesel and diesel-async on Postgres. Schema mistakes fail to compile.
riverbase_core

Access

Identity
Keycloak / OIDC JWTs, sessions, CSRF, profiles; MockAuth for development
riverbase_http
Authorization
Casbin on every route, deny by default; tenant and scope policy on every row
riverbase_http

Consistency

Audit trail
Command, event, activity and response rows commit with the state change
riverbase_core
Outbox
Integration messages leave after commit, at least once, to NATS, Redis or Postgres
riverbase_core

Runtime

Concurrency
ractor actor pools for commands, a semaphore for reads, one transaction per command
riverbase_core
Deploy modes
Coupled or split command/query processes from the same domain crate
riverbase_http

Batteries

Extensions
Bus workers and job tracking, media storage on OpenDAL, declarative forms
riverbase_*

Cargo workspace · 6 crates

Start with three crates. Add the rest when a domain needs them.

All crates

Feature flags

Every crate is a git dependency. Turn on only the features you use:

cargo add riverbase_http --git https://github.com/riverbase-io/riverbase
  • riverbase_core/nats-ioNATS message bus for the outbox and workers
  • riverbase_http/websocket,sse,rxdbRealtime: WebSocket bridge, SSE, RxDB replication
  • riverbase_file/aws-s3S3-compatible media storage
  • riverbase_task/nats-ioWorkers that consume NATS subjects

Five minutes to a running API

~/todo-app
1Add the crates
cargo add riverbase_core riverbase_proc riverbase_http --git https://github.com/riverbase-io/riverbase
Updating git repository `https://github.com/riverbase-io/riverbase`
Adding riverbase_core (git) to dependencies
Adding riverbase_http (git) to dependencies
2Declare the domain
cat domain/src/domain.rs
riverbase_core::domain! {
domain TodoDomain {
meta { namespace: "riverbase.todo", title: "Todo", description: "…" }
command: TodoCommandEngine,
query: TodoQueryEngine,
spawn(runtime: &DomainRuntime) { /* … */ }
}
}
3Run it
cargo run -p todo-service
Compiling todo-service v0.2.0
INFO database ready (framework + todo migrations applied)
INFO listening on 0.0.0.0:8080
curl -s -XPOST :8080/api/riverbase.todo/create-todo:post/todo -H 'content-type: application/json' -d '{"payload":{"title":"Ship it"}}'
{"id":"7f3a91c2-…","title":"Ship it","notes":""}

Open /api/openapi.json for the generated API document. Output shown is illustrative.

Start with the quick start.