v0.1 pre-release · Apache-2.0 Start an evaluation

Build on Trellara

Protocol specification

Developer The wire contract, field by field, for anyone writing a consumer that is not ours.

The transaction envelope is the neutrality claim. If it is a private encoding, “destination-neutral” is marketing; if it is a protobuf schema you can generate from, it is a property of the system. This page is the second thing.

Where the authority is

The schema exists twice and both must agree: crates/trellara-protocol/proto/trellara/v1/envelope.proto is the IDL, and the prost attributes on the Rust types are what the code encodes. If this page ever disagrees with either, they win. A conformance suite that would let you prove your consumer correct does not exist yet — it is named in the sidebar rather than quietly implied.

Version

PROTOCOL_VERSION is 1. An envelope whose protocol_version differs is rejected on decode. There is no best-effort interpretation of an unknown version, and a consumer you write should behave the same way.

The envelope

Thirteen fields. The tag numbers are the compatibility surface — treat them as fixed.

TagFieldTypeRule
1protocol_versionuint32Must equal 1.
2source_idstringRequired, non-blank.
3database_idstringRequired, non-blank.
4dataset_idstringRequired, non-blank.
5transaction_idstringRequired. Every change and DDL event must repeat it.
6begin_lsnstringOptional. When present, must parse and be ≤ commit_lsn.
7commit_lsnstringRequired, canonical, non-zero.
8commit_timestamp_msint64Required, greater than zero.
9schema_versionsrepeatedRelation schema identities the changes reference.
10changesrepeatedOrdered ChangeRecord values.
11manifestoptionalPresent only for chunked and partitioned boundaries.
12checksumuint64xxh3_64 of the encoded message with this field zeroed.
13ddl_eventsrepeatedOrdered DDL events sharing the total_order space with changes.

One order across DML and DDL

total_order is a single sequence covering both changes and ddl_events. A duplicate total_order between a change and a DDL event in the same transaction is a validation error.

That rule is what stops an event class from disappearing during chunking or reconstruction: you cannot reassemble a transaction, count only its rows, and conclude it is complete. Operator surfaces render the two counts and their sum as dml=N ddl=M source_total=N+M; source_total is a rendering, not a wire field, so do not look for it in the schema.

ChangeRecord

FieldNotes
transaction_idMust match the envelope.
total_order, table_order, partition_orderThree orderings, for the transaction, the relation and the partition lane.
relationRelation identity.
operationinsert, update, delete, truncate.
replica_identitydefault, index, full, nothing.
before, afterOptional row images. A column may be marked unchanged-TOAST; see below.
idempotency_keyRequired, non-blank.

Event identity

The canonical ordered event key is deterministic from four values:

{source_id}:{commit_lsn}:{transaction_id}:{total_order}

TransactionBoundaryKey additionally carries database_id, which participates in its display form but not in the event key. Deduplicate at the transaction level; the record-level key exists so inspection and lake planning are deterministic, not because apply needs it.

Checksum

Compute it by cloning the message, setting checksum to zero, encoding, and hashing the bytes with xxh3_64. The reference implementation validates then verifies before producing bytes, and verifies then validates on the way in — so a consumer never sees a partially-trusted envelope. Do the same.

Manifests and commit markers

A plain strict transaction carries no manifest. Chunked and partitioned boundaries carry both a manifest and a commit marker, and the marker binds the manifest by checksum so the two cannot be mispaired.

TransactionManifestPurpose
transaction_id, source_commit_lsn, source_commit_timestamp_msIdentity, repeated from the envelope.
global_event_countTotal events the complete transaction must contain.
partitionsNon-empty. Each entry: id, event_count, first_total_order, last_total_order, checksum.
affected_tablesPer-relation event counts.
boundary_modestrict_chunked_transaction_order or partitioned_scale_mode.

Manifest validation rejects a zero first_total_order or last_total_order, a reversed range, a zero event_count, an event_count larger than the declared range span, and a last_total_order above global_event_count. Implement all of them: they are what makes a truncated or fabricated manifest detectable.

TransactionCommitMarker repeats the identity, LSN, timestamp and global_event_count, adds participating_partition_count, and carries manifest_checksum.

Rules a correct consumer follows

Reading the local log directly

If you are consuming the brokerless log rather than a broker, the frame format is a four-byte magic, a u32 body length, the encoded body, and a u32 CRC32 of the body. The current magic is TLG2; TLG1 frames remain readable so an existing log survives an upgrade. Any other magic, a body length above the 128 MiB cap, or a checksum mismatch is corruption — not a record to skip. A short read at a frame boundary is a torn tail, and the correct response is to stop at the last complete valid frame.

On this page