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

Persistence, audit, and messaging

The datastore​

Persistence lives in riverbase_core::datastore:

TypeRole
DataStoreResource-keyed reads, queries, writes, hard remove, and cache invalidation
ResourceRegistryAllow-list that maps public resource strings to ResourceName
PgDataStoreThe Postgres implementation, dispatching each resource to a registered entity
DataStoreInitBuilds the store for a domain from its DomainStoreSpec

DataQuery carries the source, client filter, server policy filter, ordering, projection, joins, and pagination. PgDataStore routes each resource to an ErasedEntity, which is usually generated by pg_domain_entity!. Custom executors can serve a query resource directly.

Postgres is the only backend. The in-memory store was removed, so tests run against a real database.

Where this is heading

DataStore serves both reads and writes and is broad enough to bypass typed invariants if used carelessly. The direction is to keep it for read models and framework metadata, and to add typed repositories for aggregate state.

Shared row metadata​

Framework-backed tables carry domain fields, declared once with diesel_table_with_domain_fields!:

ColumnMeaning
_idRow UUID
_created, _creatorCreation time and actor
_updated, _updaterLast update time and actor
_etagOptimistic concurrency token
_tenantTenant UUID, stamped at creation
_deletedSoft-delete marker

AggregateCore fills these from AggregateContext on every write. _tenant is a UUID from the caller's authorization context (usually the organization). It is not the auth realm string.

Audit channels​

riverbase_core::logstore defines one append-only store per channel, bundled as DomainLogStore:

ChannelWritten by
context and commandThe command engine, before dispatch
eventA successful #[domain_action] (or enqueue_action_event)
messageAggregateCore::emit_message
activityAggregateCore::emit_activity
queryThe query engine (off by default)
response and idempotencyThe command engine, after dispatch

Each channel can be switched off in [riverbase.audit_log]. A disabled channel gets a no-op store. On Postgres the tables live in the riverbase_audit schema. A command row is logged as Created and updated to a terminal success or error status when the unit of work completes.

Messages and the outbox​

MessageBus is the publication port. Implementations cover NATS (the production default), Redis, and Postgres LISTEN/NOTIFY. All three also implement StreamBus, the subscribe side that fans realtime updates out across replicas.

Messages emitted during a command become outbox rows in the same transaction as the state change. After commit, the outbox publisher, started by DomainRuntime::from_config, delivers them:

  • at least once: a crash after publish and before marking can redeliver;
  • with retries: failed deliveries are retried;
  • so consumers must be idempotent.

Durable workflows​

Multi-step work that spans transactions or domains uses a process manager: rows in ProcessManagerStore with optimistic concurrency and leased claiming (claim_due), driven by spawn_process_manager_worker. Register handlers on DomainRuntime::process_registry.

Job scheduling by itself (riverbase_task) does not make a workflow durable.

Deletion​

DataStore::remove and invalidate are low-level primitives. They do not decide whether a root may be deleted, what happens to its children, or what retention applies. Every aggregate root declares its deletion semantics: soft, hard, or prohibited.

Consistency guarantees​

SituationGuarantee
State and audit in one Postgres databaseState, status, response, idempotency, audit effects, and outbox rows commit atomically
State and audit in different databasesWeaker; the deployment must state and reconcile it
Handler opens its own connection mid-commandOutside the transaction: non-conforming
Outbox deliveryAt least once, after commit

Data errors surface as structured RiverbaseError values with DAT-* codes. See Errors.