Chuyển tới nội dung chính

1. Key concepts

Before writing code, learn the words Riverbase uses everywhere: in macros, routes, policy rows, and audit records.

Domain, namespace, and names​

A domain is a bounded context. It exposes write operations (commands), read operations (queries), and optional custom routes, all under one namespace.

TermExampleMeaning
Namespaceriverbase.todoDotted domain identifier
Command keycreate-todoLocal, dash-separated name in the command registry
ResourcetodoThe collection or object type commands and queries target
Fully qualified nameriverbase.todo/create-todonamespace/key, via FqName
Integration topicriverbase.todo.createdName of a message published to the bus

Command keys are verb-noun names (create-todo), never resource:verb.

State-based DDD and CQRS​

Riverbase keeps aggregate state as the source of truth. It does not use event sourcing:

  • Commands request authoritative state changes through the command engine.
  • Queries and reports read through the query engine and never mutate state.
  • Events and activities are audit and integration facts. Nothing replays them to rebuild state.

DDD and state-based CQRS explains why.

Engines and the domain bundle​

SlotDeclared withRole
Commandcommand_engine!Write path: collection (:post) and object (:exec) commands
Queryquery_resource! + query_engine!Read path: .list, .item, .meta, and .rept reports
Servicea type implementing DomainServiceEngineOptional capabilities outside CQRS HTTP, such as ping
Domaindomain!Namespace metadata, engine slots, spawn, optional custom routes

domain! generates a type such as TodoDomain<S> that implements the Domain trait. S is the store type, usually PgDataStore.

Aggregate and EngineContext​

  • An aggregate is the command session for one dispatch. Your type, such as TodoAggregate, wraps AggregateCore, which loads the root row, writes rows, and buffers activities and messages.
  • Domain methods that change state are marked #[domain_action(event = "…")]. When the method returns Ok, Riverbase records one event for it.
  • EngineContext travels with every command and query: namespace, actor, profile, tenant, roles, server-resolved claims, correlation id, and idempotency key.

One store for writes and reads​

Command and query engines share one DataStore. In the todo service it is a PgDataStore with the todo_items entity registered. A per-domain DomainStoreSpec declares the resources, entities, and migrations. Earlier versions split this into StateStore and DataStore. The string-and-JSON state helpers now live on DataStore.

The HTTP surface​

Routes are generated from the registered commands and resources. The default api_base is /api.

OperationPatternExample
Collection commandPOST /{api_base}/{ns}/{cmdkey}:post/{resource}POST /api/riverbase.todo/create-todo:post/todo
Object commandPOST /{api_base}/{ns}/{cmdkey}:exec/{resource}/{id}POST /api/riverbase.todo/update-todo:exec/todo/{id}
ListGET /{api_base}/{ns}/{resource}.listGET /api/riverbase.todo/todo.list
ItemGET /{api_base}/{ns}/{resource}.item/{id}GET /api/riverbase.todo/todo.item/{id}
MetadataGET /{api_base}/{ns}/{resource}.metaFields, labels and sort options
ReportPOST /{api_base}/{ns}/{report}.reptParameterized report; parameters in the body

The full list, including scoped, :link, and :hook routes, is in HTTP conventions.

Authentication, authorization, row policy​

Three separate checks happen at different layers:

LayerQuestionMechanism
AuthenticationWho is calling?An auth provider: MockAuth locally, Keycloak/OIDC JWTs, or sessions
AuthorizationMay they run this operation?Casbin on each route: {cmdkey}.execute, {resource}.query
Row and tenant policyWhich rows may they see or change?The _tenant filter plus each resource's scope_policy

Authentication alone never grants permission. An empty Casbin policy denies everything.

Composing the application​

RiverbaseApp, from riverbase_http, is the entry point:

  1. RiverbaseApp::init loads riverbase.toml and env config and sets up logging.
  2. DomainRuntime::from_config connects the database, audit log, message bus, and invokers.
  3. TodoDomain::spawn(&runtime) starts the engines.
  4. app.mount_domain(domain) registers command, query, and custom domain routes.
  5. app.mount_auth_routes() adds /api/auth/* when auth is configured.
  6. app.serve() binds and runs axum.

Next: 2. Scaffold and domain.