Entry format v1
An entry is one JSON object, stored as its canonical bytes (RFC 8785) on one line. Concepts are in Streams and entries and Integrity.
General rules
Section titled “General rules”- Any top-level member not listed below fails with
unknown_field. Any other member inside the nested objects fails withinvalid_field. - An optional member that is
nullis the same as an absent one; writers omit it. - All strings named below (ids, refs, versions, types, targets, handles) must be non-empty. Arrays
may be empty. Values inside
extare free, within the canonical-bytes rules. - Numbers are integers only, between -(2^53 - 1) and 2^53 - 1.
Encodings
Section titled “Encodings”| Value | Encoding |
|---|---|
| Hash | sha256: and 64 lowercase hex digits |
| Public key | ed25519: and the 32-byte key (RFC 8032) as 64 lowercase hex digits |
| Signature | ed25519: and the 64-byte signature as 128 lowercase hex digits |
| Key fingerprint | sha256: and the lowercase hex SHA-256 of the 32-byte public key |
| Stream id | tnb1: and the first 32 hex digits of the genesis stream key’s fingerprint |
| Entry id | A ULID: 26 characters of Crockford base32, uppercase, first character 0 to 7 |
| Timestamp | YYYY-MM-DDTHH:MM:SS.mmmZ: UTC, exactly three fraction digits, a valid calendar date |
| Key id | 1 to 128 characters from A-Z a-z 0-9 . _ : - |
| Body address | blob:ct-sha256: and 64 lowercase hex digits |
| Input digest | hmac-sha256: and 64 lowercase hex digits |
Every encoding has exactly one spelling: uppercase hex, a lowercase ULID or a timestamp without its milliseconds is invalid.
Fields
Section titled “Fields”“Filled by” says who sets the member: the writer (the library, tenbri append or tenbrid)
or the caller (the draft you send).
| Field | Type | Required | Filled by | Description |
|---|---|---|---|---|
v |
integer | always | writer | Format version, 1 |
id |
string (entry id) | always | writer, or caller | Unique within the stream. The writer generates it when the draft has none; tenbrid takes it from the Idempotency-Key. |
stream |
string (stream id) | always | writer | tnb1: id of the stream; also the checkpoint origin |
seq |
integer | always | writer | Starts at 1, increases by exactly 1; at most 2^53 - 1 |
ts |
string (timestamp) | always | writer, or caller | Time claimed by the writer, not proven |
hlc |
object {wall, count} |
no | caller | Hybrid logical clock: wall milliseconds since the Unix epoch, count a counter, both non-negative integers. Never decreases within a stream. |
trace |
object {trace_id, span_id} |
no | caller (tenbrid: from traceparent; the Rust writer with its otel feature: from the active span) |
OpenTelemetry span active when the entry was written: 32 and 16 lowercase hex digits |
durability |
string | no | caller (tenbrid fills intents) |
Promise that applied when the intent was acknowledged |
group |
string | no, only on seq 1 |
caller | Stream group the lane belongs to (group keys reserved) |
kind |
string | always | caller (tenbrid fills it per endpoint) |
See kinds |
phase |
string | per kind | caller (tenbrid fills it per endpoint) |
intent or outcome |
intent |
string (entry id) | per kind | caller | The intent an outcome or reconcile closes |
status |
string | per kind | caller | Result of an outcome or reconcile |
source |
string | per kind | caller (tenbrid default: sdk) |
Where the entry came from |
degraded |
boolean | no | caller | true when written in fail-open mode |
principal |
object {kind, id} |
per kind | caller (tenbrid: the token’s) |
Whoever acted |
action |
object {type, target} |
per kind | caller | type the action type; target optional, a reference, possibly personal data |
subject |
string | no | caller | Pseudonymous handle selecting the per-subject keys used for erasure (Privacy) |
inputs |
array of {ref, version, digest} |
no | caller (the Python and TypeScript bindings: from a run’s reads) | ref and version plaintext, digest an input digest (keyed) |
rule |
object {ref, version, result} |
no | caller | The rule that allowed or refused. Must be absent when source is proxy or ingest. |
approval |
object {by, entry} |
no | caller | by the approver (a principal object), entry the entry hash of an earlier approval entry in this stream |
links |
array of {stream, seq, hash, rel} |
no | caller | Entries in other streams; rel optional. Checked for shape only (following a link is reserved). |
body |
string (body address) | no | caller | Address of the encrypted body, as DataKeys::seal_body in tenbri-crypt returns it (Privacy) |
attestations |
array of {key, sig} |
no | caller | Extra signatures by the principal or an approver |
ext |
object | no | caller | Experimental features. Hashed and signed like everything else; meaning may change while a feature is Experimental. |
key |
object | per kind | writer | The key event of a key entry; see key events |
prev |
string (hash) | every seq but 1 |
writer | Entry hash of the previous entry; absent on seq 1 |
signer |
string (key id) | always | writer | Key that signed the entry |
sig |
string (signature) | always | writer | Ed25519 over tenbri/entry/v1, 0x00, and the canonical entry without sig |
A draft sent to tenbri append or tenbrid that contains v, stream, seq, prev, signer
or sig is refused with invalid_field ( `<member>` is filled in by the writer).
Allowed values
Section titled “Allowed values”| Member | Values |
|---|---|
kind |
decision, approval, key, reconcile, note |
phase |
intent, outcome |
status |
ok, error, refused, unknown |
source |
sdk, proxy, ingest |
durability |
local, replicated, postgres |
principal.kind |
agent, person, service |
links[].rel |
cause, handoff, approval |
key.op |
certify, rotate, revoke |
key.role |
stream, session |
rule.result is a free, non-empty string.
Rules per kind
Section titled “Rules per kind”| Kind | Requires | Forbids |
|---|---|---|
decision, phase intent |
phase, source, principal, action |
intent, status, key; source: ingest |
decision, phase outcome |
phase, source, principal, action, status; intent unless source is ingest |
key; intent when source is ingest |
approval |
principal (the approver), action (what is approved) |
phase, intent, status, key |
key |
key |
phase, intent, status |
reconcile |
intent, status: unknown |
phase, key |
note |
nothing | phase, intent, status, key |
Across the stream:
- An outcome names an intent that appears earlier and has no outcome yet (
bad_intent). - A reconcile names an earlier intent with neither an outcome nor a reconcile (
bad_intent). An outcome that arrives after a reconcile is still accepted and supersedes it. - Writers reconcile an intent with no outcome after 1 hour by default.
approval.entrynames the entry hash of an earlierapprovalentry (bad_approval).keyandreconcileentries are written bytenbriandtenbridthemselves;tenbridrefuses them in a draft.
Key events
Section titled “Key events”The key member of a key entry. Details in Keys.
op |
Members | Signed by | Effect |
|---|---|---|---|
certify |
id, pub, role (stream or session), not_after (sessions only) |
The current stream key | Adds a key. role: stream only in the genesis entry, which the new key signs itself. |
rotate |
id, pub |
The current stream key | The new key becomes the current stream key |
revoke |
id, from (a sequence number, at least 1) |
The current stream key | Entries signed by key id at from or later fail with key_revoked, including earlier-written ones |
Session keys are certified for 24 hours by default; an entry with a ts later than its session
key’s not_after fails with key_expired.
Attestations
Section titled “Attestations”An attestation’s sig is the Ed25519 signature of key over tenbri/attestation/v1, 0x00, and
the canonical entry without seq, prev, signer, sig and attestations. A principal or
approver signs the draft (its id, ts and stream already set) before the writer places it.
Example
Section titled “Example”The intent entry from
spec/vectors/entries.json,
pretty-printed here. Stored, it is one line with no whitespace.
{ "action": { "target": "zendesk:ticket/4821", "type": "reply_ticket" }, "body": "blob:ct-sha256:305531dcc50ebca31cf1d5b31e9fc76ed51f66b3b6dd5a030c6539ae6532f979", "durability": "local", "hlc": { "count": 0, "wall": 1791986533318 }, "id": "01M4XBH0Y60000000000000003", "inputs": [ { "digest": "hmac-sha256:80dadc04df5bae022b918452d942e5ee756e6cf3a7f8ac8f462c8f9a521ac847", "ref": "policy:refund", "version": "14" }, { "digest": "hmac-sha256:61eb70aa07259d108510291f2f6e0eb8f216dc1138c119b6ec3a122ab3f3c573", "ref": "contract:acme-sante", "version": "3" } ], "kind": "decision", "phase": "intent", "prev": "sha256:3b75c4e724b4379a38fd76be9b9cee650516210c57a4236c624804cc6eaf72f0", "principal": { "id": "support-assistant", "kind": "agent" }, "rule": { "ref": "grants:support-assistant", "result": "allow", "version": "7" }, "seq": 3, "sig": "ed25519:7bbb976272f395acd659aa26289b113761cf3af937c1609b64ca658d8494a0fc7cc46cb50177b79f64c932912e09b198df497d43fda6e393a2e6e70517f9eb0e", "signer": "sess:a1", "source": "sdk", "stream": "tnb1:170241c12e8724974ea3b10fe536b7d8", "subject": "subj_7f3c91d2", "trace": { "span_id": "00f067aa0ba902b7", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736" }, "ts": "2026-10-14T14:02:13.318Z", "v": 1}The genesis entry of the same stream, as stored:
{"id":"01M4XBGYZP0000000000000001","key":{"id":"stream:a","op":"certify","pub":"ed25519:26ea1e8cd907cac3977eecd8cee7fa3d13aa0303ce53ce7fe359c3ebf17c525d","role":"stream"},"kind":"key","seq":1,"sig":"ed25519:d2b825716f782a98b54cffca6c2996837d11d5c0a52324bf7f21d0bc8b05c8b0306a9684ba3878e3dc848493353914c836b9991a2bc3bba1f57e38981869c501","signer":"stream:a","stream":"tnb1:170241c12e8724974ea3b10fe536b7d8","ts":"2026-10-14T14:02:11.318Z","v":1}