Skip to content

Protocol reference

Reference

Colla defines deterministic Value and Change bytes, then wraps them in small versioned envelopes for local Snapshots and application Updates. The body codec is shared by Rust and JavaScript; transport policy remains yours.

Use this page when implementing storage, a server boundary, or a second language binding. For the semantic model behind the bytes, read Data model and the OT guide.

Four wire shapes

ShapePayloadVersioned?Intended use
Value bodyOne Core ValueNoNested content at a Core boundary.
Change bodyOne Core ChangeNoAn operation relative to a base Value.
COLLAS Snapshotrevision: u64 + content: ValueProtocol 1Local content checkpoint and restore.
COLLAU Updaterevision: u64 + updateId: u64 + change: ChangeProtocol 1Application-owned transport of one operation.

The body formats are versionless by design. Do not infer a document identity, author, timestamp, or server sequence from a Value or Change body.

Core Value tags

The first byte of a Value body selects one closed variant:

TagValue kindPayload
0x00NullNone.
0x01BoolOne byte, 0x00 or 0x01.
0x02IntSigned i64, zig-zag then shortest unsigned varint.
0x03FloatLittle-endian IEEE-754 f64; only finite values are valid.
0x04StringUTF-8 byte length and bytes; atomic text.
0x05TextUTF-8 byte length and bytes; collaborative text.
0x06RichTextSpan count followed by RichText spans.
0x07ListElement count followed by Values.
0x08MapEntry count followed by key/Value pairs.

Map keys are UTF-8 strings in strictly increasing lexical order. A duplicate or out-of-order key is not a canonical body. Text lengths count Unicode scalar values in Change operations, while the encoded content itself is UTF-8.

Core Change tags

TagChange kindPayload
0x00NoopNone.
0x01ReplaceOne Value.
0x02MapOrdered map entry changes.
0x03ListSequence operation stream.
0x04TextRetain/insert/delete stream.
0x05RichTextContent and attribute operation stream.
0x06IntSigned addition delta as zig-zag varint.

Map entries use Insert, Delete, or recursive Modify. List operations use Retain, Insert, Delete, or one-element Modify. Text operations use Retain, Insert, and Delete. RichText operations use Retain with an optional attribute patch, Insert of text or one atomic embed, and Delete.

Constructors produce the semantic canonical form: zero-length operations and empty inserts are removed, compatible adjacent operations are merged, and trailing plain retains are omitted. Change does not include a base revision, old values, author, or operation ID.

RichText payloads

RichText is encoded as a span count followed by spans. A span carries either text content or one atomic embed, then its canonical attributes:

Span contentMeaning
TextUTF-8 text; adjacent text with equal attributes is merged by constructors.
EmbedOne nested Value; an embed has sequence length one and is not recursively edited in RichText.

Attribute values are Bool, signed Int, finite Float, or String. Attribute keys are strictly increasing. A retain patch contains explicit Set and Remove actions; Null is not a deletion sentinel.

Local envelopes

Both envelopes have the same eight-byte header:

text
bytes 0..5  magic ASCII (`COLLAS` or `COLLAU`)
bytes 6..7  protocol version, unsigned u16 little-endian (`1`)
bytes 8..   cocodec tuple payload

The payloads are:

text
Snapshot: (revision: u64, content: Value)
Update:   (revision: u64, updateId: u64, change: Change)

In an Update, revision is the base revision at which the Change was created. In a Snapshot, revision is the visible content revision. updateId starts at 1 for each JavaScript Document instance and is used for cumulative acknowledgement correlation. It is not global identity and is not stored in a Snapshot.

In JavaScript, Snapshot and Update are pure data objects that expose their canonical envelope bytes directly via .bytes: Uint8Array:

ts
import { Document, Snapshot, Update } from 'colla-ot'

const document = Document.fromJS('Draft')
const snapshotBytes = document.snapshot().bytes
const snapshot = Snapshot.decode(snapshotBytes)

const update = document.transact(tx => tx.set([], 'Draft v2'))
const updateBytes = update.bytes
const received = Update.decode(updateBytes)

Rust exposes the same envelope types as Snapshot::new/decode and Update::new/decode. See the Rust API for signatures.

Strict decoding

The shared decoder must consume one complete value, change, or envelope. It rejects:

  • wrong envelope magic or unsupported protocol version;
  • truncated input, lengths beyond the remaining bytes, and unknown tags;
  • invalid UTF-8, non-minimal varints, invalid bool bytes, and out-of-range integers;
  • duplicate or out-of-order map/attribute keys;
  • non-finite floats and other invalid Value construction data;
  • trailing bytes after a complete body or envelope;
  • recursion depth or other built-in resource limits being exceeded.

Rust reports these as CodecError; the JavaScript facade maps them to CollaError with code: 'invalid_encoding' or code: 'limit_exceeded'. Error messages and byte offsets are diagnostics, not a cross-version wire contract. See Glossary and errors for the complete taxonomy.

Byte canonical versus semantic normalization

The codec enforces byte-level rules such as shortest varints, valid UTF-8, key ordering, known tags, and complete input. Semantic normalization belongs to constructors and algebra: they remove empty operations, merge compatible operations, and collapse no-op changes. A decoder should therefore not be used as a substitute for Change construction validation; decode, then apply against a concrete base when compatibility matters.

cocodec supplies fixed recursion and safe length handling for byte decoding. The JavaScript InputLimits policy applies to structured fromJS input, not to the canonical body or to OT results. See JavaScript API for the default structured-input limits.

Application responsibilities

The Colla envelopes intentionally leave these concerns to an outer protocol:

ConcernProvide outside the envelope
IdentityDocument ID, tenant, author, client/session ID.
OrderingServer sequence, confirmed revision policy, and delivery order.
ReliabilityRetries, acknowledgements, deduplication, and replay handling.
SecurityAuthentication, authorization, signatures, and encryption.
StorageSnapshot cadence, outbound queue, history, and migrations.
Encoding policyCompression, checksums, framing, and content type.
UI statePresence, cursors, selections, and editor-specific formats.

The JavaScript Document helper accepts server-ordered Updates at the next confirmed revision, rebases pending local edits with a fixed left-first tie-break, and acknowledges pending Updates in FIFO order. It does not supply a network transport, session protocol, global deduplication, or crash-recovery queue. Read the Document state before designing that outer protocol.

Compatibility checklist

When implementing another reader or writer:

  1. Treat all integers and revisions as unsigned/signed widths shown above.
  2. Encode map and attribute keys in strict lexical order.
  3. Use shortest varints and valid UTF-8; consume the entire input.
  4. Keep Value/Change bodies separate from COLLAS/COLLAU envelopes.
  5. Preserve updateId as local correlation only; never use it as global identity.
  6. Add application metadata in an outer frame, not by changing a body silently.
  7. Run the shared golden fixtures and round-trip tests before shipping.

Colla v0.3.0 · Released under the MIT License.