Liking cljdoc? Tell your friends :D

Wire Protocol

Control plane: gRPC (tonic/prost). Data plane values: a Corium-defined tagged binary encoding carried in protobuf bytes fields. Segments never travel over gRPC — peers read the blob store directly.

Value wire encoding

The sortable segment encoding (data-model.md) is also the wire encoding for single values; composite payloads (tx-data, query args, results) use a length-prefixed tagged variant of the same tag space extended with container tags (list, vector, map, set) and an interning table per message for keywords and repeated strings. One corium-protocol::codec module owns both variants; round-trip and cross-variant property tests keep them honest.

Rationale (ADR-0006): protobuf handles framing, streaming, auth, and versioning where it is strong; EDN's open value set lives in one codec we control, rather than being contorted into protobuf messages.

Services

TransactorService (peers → transactor)

service Transactor {
  rpc Transact(TransactRequest) returns (TransactResponse);      // tx-data bytes → tempids, basis, tx-data
  rpc Subscribe(SubscribeRequest) returns (stream TxReport);     // declares client basis; server backfills then streams
  rpc Sync(SyncRequest) returns (SyncResponse);                  // wait for basis ≥ t
  rpc Status(StatusRequest) returns (StatusResponse);            // basis, index-basis, lease info, stats
}
  • Subscribe is the peer's lifeline: tx-reports, index-basis announcements, and heartbeats are multiplexed on this stream. The handshake advertises the server's heartbeat interval; a stream silent for three intervals is presumed dead and dropped even when the transport has not noticed. Disconnect ⇒ peer reconnects, rotating through its endpoint preference list (an HA standby rejects the subscription with a standby FAILED_PRECONDITION until it holds the lease; peers with storage credentials can also rediscover the holder's advertised endpoint from the root record) and resubscribes from its basis; the transactor backfills from the log if the gap is large.
  • On a cold storage-aware connection, the initial subscription basis is the index-basis-t of the immutable snapshot the peer just loaded. The root selects a complete snapshot, and the subscription supplies the gap through the handshake basis, so concurrent index publication does not require a cross-service transaction.
  • All requests carry the database name and a protocol version; the transactor rejects mismatched format-version roots with a clear upgrade error.
  • Protocol v2 adds TransactRequest.expected_basis_t. When present, the transactor rejects a stale request before transaction preparation or durable append; peer-local read/modify/write adapters use this fence.
  • Protocol v3 adds the sealed value tag 0xA6class:8 ‖ epoch:4 ‖ vtype:1 ‖ body — and the versioned schema payload that carries protection classes and per-attribute protection timelines. The tag is 0xA6 rather than the 0xA0 the sortable encoding uses, because 0xA0 is LIST in the composite tag space. A pre-v3 client never receives one: values reach a thin client through the boundary EDN, which renders an unhydrated value as #corium/redacted {:class … :type …}, a tagged element any EDN reader parses. Sending sealed values on the wire arrives with the peer server's deferred --seal-through mode.
  • The schema payload opens with a version marker. Version 1 opened with an attribute count, which no schema comes near, so a reader tells the two apart by inspection: a current build still reads a database written by an older one, and a payload from a newer build reports an upgrade rather than mis-parsing.
  • Version checks use a supported range to permit server-first rolling upgrades. A v3 server accepts v1 and v2 clients (which cannot request the basis fence, or receive a sealed value); a v3 client still sends version 3 and is rejected by an older server before that server can ignore what it does not understand. Upgrade transactors first, then peers and clients.
  • The proposed schema-migration protocol adds schema_basis_t and schema_generation to the handshake and schema_generation_after to tx reports. The handshake schema is the snapshot effective at the subscriber's from_basis_t, not the server's newest schema. Reports with t > from_basis_t advance data and schema together. A subscriber from basis 0 receives the pre-basis schema seed in the handshake, never as a t = 0 report. These semantics require a protocol-version bump so an older peer cannot silently install a current schema before replaying older data.

Future fleet routing

The current peer connects to an ordered transactor endpoint list. The proposed transactor fleet design replaces that deployment contract with one fleet endpoint while preserving the database field as the authoritative request target.

The SDK will duplicate a canonical database routing key in gRPC metadata so an L7 load balancer can apply advisory consistent-hash affinity. Any transactor ingress may receive the request; owner-dependent work is executed locally or forwarded once to the lease holder. Structured NotOwner details replace parsing standby/deposed message text. Affinity never grants ownership.

Transparent retry after a request reaches the owner additionally requires a durable transaction request ID and result deduplication. Until that protocol exists, an in-flight connection loss remains ambiguous exactly as it is today.

CatalogService (admin)

CreateDatabase, DeleteDatabase, ForkDatabase, ListDatabases, GcDeletedDatabases, index controls, and GetBackupInfo. Most are thin wrappers over root-store operations plus transactor bootstrap datoms. ForkDatabase creates a new database duplicating an existing one at a transaction basis by copying the log prefix; the fork replays it and publishes indexes of its own. GetBackupInfo briefly serializes with commit to return a definite current basis and the underlying storage connection; the backup client then leaves the transactor and reads the bounded native log range directly.

Schema migration adds one administrative call:

rpc AlterSchema(AlterSchemaRequest) returns (AlterSchemaResponse);

There is no PlanSchemaUpdate. Planning is a pure function of a desired schema and one immutable database value, and a peer already holds that value, so it plans locally against its own Db and needs only Inspect. Adding a round trip would buy nothing and would let the plan disagree with the value the operator is looking at.

AlterSchema carries the desired schema as normalized EDN attribute maps — not the plan — alongside the plan digest, installed-schema fingerprint, observed basis, --prune mode, allowances, acknowledgements, and tool version. The transactor recomputes the logical plan and its digest from that schema and the schema installed under its own writer queue, and refuses unless the digest matches. A digest it could not re-derive would be an opaque token rather than a precondition. Advisory counts are not part of the digest, so data drift that preserves every precondition does not invalidate a plan. The response reports the resulting basis, schema generation, the idents installed, and whether anything changed at all. Catalog never depends on the operator service.

The schema generation is derived per database value rather than carried on the wire: peers apply schema datoms in tx reports and converge on it. Carrying it explicitlyschema_basis_t and schema_generation on the handshake, schema_generation_after on tx reports, and the schema generation plus per-attribute AVET readiness basis on published-root and status metadata — is still proposed, and is what a protocol-version bump would gate so an older peer cannot silently install a current schema before replaying older data. The forced backfill that bypasses interval/tail pacing belongs with it, as does the rule that readiness is never reported ahead of the root that proves coverage.

OperatorService (operator tools → operator peer service) (proposed)

Job submission, planning, approval, cancellation, and watching; schedules; and fleet, database, storage, and key inspection — with a JSON/HTTP gateway over the same surface for the UI and for scripting. It is a separate service from Catalog on purpose: the transactor's latency belongs to the commit pipeline, so it is called by the operator service rather than extended into it. See operator-service.md.

PeerServerService (thin clients → peer server)

For languages without the peer library; queries run server-side on a hosted peer:

service PeerServer {
  rpc Query(QueryRequest) returns (stream QueryResultChunk);
  rpc Pull(PullRequest) returns (PullResponse);
  rpc Transact(TransactRequest) returns (TransactResponse);      // proxied
  rpc Datoms(DatomsRequest) returns (stream DatomChunk);
  rpc TxRange(TxRangeRequest) returns (stream TxChunk);
  rpc DbStats(DbStatsRequest) returns (DbStatsResponse);
  rpc Subscribe(SubscribeRequest) returns (stream TxReport);     // relayed
}

Requests name a db view as {db-name, as-of?, since?, history?, as-of-instant?, since-instant?} so thin clients get the full time model, by basis or by wall clock. Result streams are chunked with a server-enforced fuel/deadline per query. This service definition plus the codec spec is the public thin-client protocol; a conformance doc and test vectors ship with it so third parties can write clients.

Security

  • TLS via tonic/rustls everywhere; mTLS or bearer-token auth per endpoint (pluggable Authenticator trait; static tokens in v1).
  • Request-scoped identity and authorization live in corium-protocol::authz (optional per-surface enforcement, external identity providers, and per-principal view decisions). Guards are wired into the transactor and peer gRPC services; a filtered decision is rejected on surfaces that cannot enforce it yet. See auth.md and ADR-0012.
  • Peer servers enforce per-request fuel, result-size, and concurrency limits; the transactor enforces tx-size and queue limits.
  • The blob store is assumed private to the deployment (peers have direct credentials to it, as in Datomic). Encryption at rest (encryption.md) is proposed to remove that assumption for blobs, log records, backups, and cached segments.
  • Attribute protection classes seal values under per-class keys before they leave the writing peer, so tx-data, tx-reports, and datom streams may carry sealed values that only a key-holding reader can hydrate. Tx-data carries one as the EDN tagged form #corium/sealed {:class :epoch :vtype :body}, which the transactor validates against its schema without holding any key. A peer server hydrates per request with its own key set; forwarding sealed values to the thin client (--seal-through) is deferred.

Embedded transport

The same service traits have an in-process implementation over channels (corium-peer talks to corium-transactor directly). Tests and the simulator run the identical pipeline code both ways; only the transport differs. This is the mechanism that lets us build "full topology" logic from day one while running single-process until M4.

Can you improve this documentation? These fine people already did:
Casey Marshall & Claude
Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close