Git Event Store Specification (GES)
Version: 0.23.0 (Draft)
Status: Draft specification, open for public alignment
Core model: Dynamic Consistency Boundaries (dcb.events)
Reference binding: Emmett (@event-driven-io/emmett) EventStore interface, over the derived stream view
0. About this document
Section titled “0. About this document”This document is the core of the GES specification family — an open specification that independent implementations can align to, so that a repository written by one implementation is readable by any other. Its trajectory is from specification to the standard for event sourcing in Git; the title “standard” is earned by independent aligned implementations, not claimed in advance. The normative on-disk format and the conformance requirements (§16) define what an implementation must satisfy. Implementation planning lives outside the specification. This document is named Git Event Store (GES); CrumblES is the maintainer’s implementation of it. Naming it here is non-normative: conformance is defined by §16 alone, and no implementation is privileged by being named.
0.0 Specification family and section directory
Section titled “0.0 Specification family and section directory”The family is this core plus optional, individually claimable extensions (§1); the global stream (§20) and the Dynamic Consistency Boundary model (§18b, §18c) are both core-normative — every conforming store maintains the global stream (C22) and exposes the DCB interface (C23). Section numbers are stable across the family: a §-reference resolves per this directory, wherever it appears.
| Sections | Document |
|---|---|
| §0–§16.2, §18, §18b, §18c, §19, §21 (minus the rows below) | this document (core) |
| §3.4 (linkage coupling, shadow capture) | extensions/linkage.md |
| §10.1–§10.3 (Emmett binding) | extensions/emmett-binding.md |
| §16b, §16c (searchability, read models) | extensions/stream-projections.md |
| §16d (JSONL export) | extensions/stream-export.md |
Each extension document carries its own EARS requirements annex; the companion
ges-review-and-ears.md holds the core families, the cross-family traceability matrix, and the
review backlog.
0.1 Requirement language
Section titled “0.1 Requirement language”The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL are to be interpreted as described in RFC 2119 and RFC 8174.
A conforming implementation is one that satisfies every MUST in §16.
1. Scope, goals, and explicit non-goals
Section titled “1. Scope, goals, and explicit non-goals”GES defines how to store an event-sourced record inside an ordinary Git repository such that events travel with source code, remain human- and agent-readable, and are reproducible.
Goals (in priority order):
- Portability & sovereignty. No server, no external database. Every clone carries complete, self-contained copies of the stores it holds, and is a full backup of them.
- Agent- and human-readable audit. The event store is plain text and files, traversable with
cat,grep,git log, and tree walks, without a driver, query language, or credentials, and co-located in-band with the code it explains. - Reproducibility. Because state is derived from events by a pure fold, any party MAY replay the events deterministically and verify that a claimed state follows from it.
- Native distribution. Fork, branch, merge, push, pull, and offline writes are first-class.
Explicit non-goals (deliberate trade-offs, not defects):
- N1. Synchronous global durability / HA. GES does not provide quorum-replicated writes. This is a CAP trade-off consciously exchanged for offline writability (§11, §12).
- N2. High-throughput concurrent writers on the same stream. GES targets the single-writer-at-a-time-per-stream invariant. Systems needing many concurrent writers to one stream at high rate SHOULD use a dedicated event store (EventStoreDB, PostgreSQL) instead.
Implementations MUST document to their users which of N1–N2 they relax, if any.
GES is infrastructure, not a framework. GES is a standalone event store — a storage format (§3–§8), an integrity model (§8), durability and distribution rules (§11–§12), and a small framework-neutral core interface: append, read, subscribe/catch-up, and fold/snapshot (§10). It is designed to be used by event-sourcing frameworks and applications, not to be one. Emmett is the first and reference binding (§10), a consumer of GES — not its definition. GES defines its own terms and interface and can equally be driven by another framework (Axon-style command handlers, a DCB library, a bespoke decider loop), by direct use, or by none. Where this spec names an Emmett symbol (e.g. streamPosition), it does so only to show the mapping; the GES term is primary and maps just as well to the equivalents in EventStoreDB, Marten, Message DB, and EventSourcingDB (and, for the stream-less concepts they share, DCB — §10.0). GES conformance (§16.1) is defined by the core, independent of any framework binding; Emmett compatibility is a separate, optional profile (§10).
One model: the multi-master substrate carrying the global stream. Sections 3–15 describe the substrate — distributed, per-stream, offline-multi-writer. Its topology is inherently multi-master: every clone is an authority over its own visibility point (§5.5.1); a remote holds no intrinsic privilege — any authority is a role assigned by convention. On this substrate every conforming store MUST maintain the global stream (§20): the primary, totally-ordered commit chain over all events, with state streams as secondary views, and one authoritative remote (default name origin) designated as the sequencer of that store’s global stream wherever more than one repository participates — a lone repository is trivially the sequencer of the stores it holds, and two stores in one repository MAY designate different remotes (§3.1.1). The global stream provides a real global total order; across repositories it reconciles exclusively by fast-forward against the sequencer (§20.6), while state streams keep conflict-free union semantics (§12.5). Offline appends remain first-class: locally visible and durable immediately, globally authoritative when sequenced (§20.5). Optional capabilities beyond this — linkage, read models, export, the Emmett binding — are extensions (§0.0); an implementation MUST declare which extensions it claims.
Alignment with established practice. Event sourcing is a well-solved problem with broad consensus across mature stores (EventStoreDB, EventSourcingDB, Marten, Message DB, Emmett). GES follows that consensus by default and deviates only where the Git substrate or GES’s stated goals require it, recording each deviation with its justification in §21. Novelty is treated as a cost, not a feature: a decision that departs from established practice without a substrate- or goal-based reason is a defect, not an innovation. §21 is the register that keeps this honest.
2. Terminology
Section titled “2. Terminology”- Stream — (concept) an ordered sequence of events identified by a key (
streamName, an arbitrary UTF-8 string chosen by the application); (realization) a Git ref plus the commit chain it points to (§2.1). The order is part of the definition — it carries the stream position (below). An event MAY belong to more than one stream (in the global stream, its state stream and the global stream, §20.2), so streams are orderings over partly-shared events, not disjoint containers. Pattern-neutral (as used by Emmett and EventStoreDB); GES does not equate a stream with a DDD aggregate. Under the global stream the global stream (a.k.a. the global log) is the primary stream and state streams are secondary, state-optimized views (§20.2). - Stream ref — the Git ref whose tip commit is the latest append to a stream.
- Event — an immutable domain fact (the concept), Emmett shape
{ type, data, metadata }. - Record — the canonical serialized form of an event (what is hashed, §6.1); the authoritative representation.
- Event file / blob — the Git object that holds a record at a tree path (§6.3.5); the substrate realization of a record.
- Append — one atomic write of one or more events to one stream, via one
appendToStreamcall. - Write vs. commit vs. event. One event is one commit (§5.2); the event’s position is that commit’s depth in its chain (in the global stream, its global stream position is its depth in the global stream — this is the event’s DCB sequence position, §18c). One write (an
appendof one or more events) is therefore one-or-more commits made atomic by a single ref transaction (git update-ref [--stdin]), not one commit. The atomic unit of a write is the ref update, not a commit. (Consequence: like DCB, GES positions events, not writes — see §18c.6.) - Stream position — the 1-based sequence number of an event within a stream. Under the global stream an event has a state stream position (in its state stream) and a global stream position (in the global stream) — the same notion applied to two streams, not two different notions. The stream version is the stream position of the tip (0 for a non-existent/empty stream) and generalizes as state stream version / global stream version. These are GES’s own terms; they map to the equivalents in other frameworks (≈ Emmett
streamPosition/globalPosition/expectedStreamVersion; EventStoreDBStreamPosition/Position; DCB sequence position). (A stream position is realized as the commit’s depth in its chain; “ordinal” and “depth” are not used as domain terms.) - Recorded metadata — store-assigned fields (position, id, timestamp, stream name) as opposed to the
metadataset by the appender/decider. - Tip — the commit a stream ref currently points to.
Roles (who does what). GES is a distributed VCS-backed store: every repository is a complete local event store and a source of truth in its own right. Three roles are named, each borrowed from where the role is actually named in practice rather than invented here:
-
Writer (also appender) — a writer appends events to the event store: it calls
appendToStreamon its local store. Local-first — a writer never appends over the network; local appends become authoritative only when the repository (as a Git client, §20.5) fast-forward-pushes to the authoritative remote. Multiple writers are permitted. “Writer” and “appender” are interchangeable and denote only this submitting role, never the ordering role (which is the authoritative remote). Precedent:write_message(Message DB),write_events(EventSourcingDB),EventAppender(Axon),append(DCB). -
Decider — the pure function (or command handler) that, given a command and current state, decides which events result. Named after the functional event-sourcing model (Emmett
decide); “command handler” is the OO synonym (Axon). -
Authoritative remote — the one remote that, by convention, holds the shared source of truth that multiple checkouts agree on. It is a role assigned to a remote, not a property of a node: there is no election and no privileged server, only an agreement about which remote is canonical (much as Git’s
originis a convention, not a technical master). The role is rebindable — the same fast-forward-only +pull --rebasemechanics work against any remote, enabling other topologies (§20.5). The default name for the authoritative remote isorigin. This deliberately avoids the store term leader (which in EventStoreDB/Axon implies a consensus-elected node with failover — machinery GES does not have) and the shared-log term sequencer (which implies a dedicated ordering component); GES’s authority is a convention over a remote, so it is named for what it is. See the §21 register. -
Canonical JSON — the serialization defined in §6.1.
2.1 Ubiquitous language — one term, several contexts
Section titled “2.1 Ubiquitous language — one term, several contexts”GES borrows vocabulary from several established fields, and in Domain-Driven Design terms each is a bounded context: a zone within which a term carries one consistent meaning, while the same word can mean something different in another context (Evans, Domain-Driven Design, 2003). “commit” in Git is not “commit” in PostgreSQL; “position” in Kafka is not “position” in DCB. This table pins, for each borrowed term, its meaning in its home context and the single meaning GES gives it — so the spec, like a ubiquitous language, stays unambiguous within GES even while reusing words from outside it. (A change to a term here is a change to the model.)
| Term | Linguistic context | Meaning in that context |
|---|---|---|
| commit | Git | A snapshot object in the DAG (tree + parent(s)); an immutable content-addressed node. |
| commit | PostgreSQL / RDBMS | The COMMIT statement: the point a transaction becomes durable and visible. |
| commit | GES | The Git object holding one event (§5.2); not the transactional point — that is the ref update (see “transactional boundary”). |
| write / append | RDBMS | A mutation of stored state (INSERT/UPDATE). |
| write / append | Event sourcing (Emmett, ESDB, Message DB, EventSourcingDB) | Appending one or more immutable events to a stream (appendToStream / write_events). |
| write / append | GES | Building one-or-more commit objects and making them visible with one ref transaction (§5.5). A writer does this against its local store (§2). |
| transactional boundary | RDBMS | BEGIN … COMMIT: atomic + durable + visible together. |
| transactional boundary | GES | The ref update (git update-ref[ --stdin]), CAS-guarded; atomic + locally visible. Durability (§11) and authoritative visibility (§20.5) are decoupled from it (§5.5). |
| stream | Event sourcing | A key with an ordered sequence of events (pattern-neutral). |
| stream | Kafka / streaming (contrast, not a store) | A topic/partition of messages; ordering only within a partition. |
| stream | DDD (mistaken usage) | Often conflated with an aggregate — GES rejects this. |
| stream | GES | (concept) an ordered event sequence under a key; (realization) a ref + commit chain. The global stream (a.k.a. global log) is the primary stream, state streams are secondary views (§20.2); an event may belong to several streams. |
| position / offset | Kafka | Offset within a partition (partition-local). |
| position | Event sourcing / DCB | A sequence position assigned per event: unique, monotonic, MAY have gaps. |
| stream position | GES | An event’s sequence number within a stream; state stream position (its state stream, Emmett streamPosition) and global stream position (the global stream, Emmett globalPosition) name the two. Realized as commit depth (§18c.6). “ordinal”/“depth” are not domain terms. |
| version | Event sourcing | Ambiguous alone; usually the stream’s high-water mark. |
| version | GES | Always qualified: stream version (a stream’s high-water mark / CAS contract, Emmett expectedStreamVersion) vs format version (gesVersion / GES-Version, the record format, §6.2/§19.1). Never bare “version”. |
| sequence position | DCB | A per-event, store-wide position: unique, monotonic, MAY have gaps. |
| sequence position | GES | The core position term (§10.0, §18c.1): the store-wide position that two distinct bounds name — the core read’s inclusive lower bound from (the event at from is included; absent means unbounded below) and an append condition’s exclusive bound after (an event at after does not fail the append; absent means any matching event fails it). ❌ “read starting after position 42” — a read starts from, a condition excludes after. It is the same number as the global stream position — DCB view vs GES stream view (§18c.6). A stream position is the per-stream number of the derived stream view. |
| append / read | GES core | The two core operations (§10.0), taking an append condition and a query — generic event-store verbs, not owned by any framework. Emmett’s appendToStream / readStream are the reference binding over the derived stream view (§10.1). |
| query | DCB → GES core | A selection of events by type and/or tags; the argument to the core read (§10.0) and the selector inside an append condition (§18c.1). |
| append condition | DCB → GES core | failIfEventsMatch + after — the query-based precondition of the core append (§18c.1, §9.1.1). Emmett’s expected stream version is one special case of it (extensions/emmett-binding.md §10.1.1). |
| sequenced event | DCB → GES core | An event paired with its sequence position, as returned by the core read (§10.0). |
| aggregate | DDD | A consistency boundary that protects invariants across entities. |
| aggregate | Emmett API | aggregateStream — the fold of a stream into state (a method name, not a boundary). |
| aggregate | GES | Not a GES concept. A stream is not an aggregate (§2); GES keeps aggregateStream only as the Emmett method name. |
| tag | Git | A named ref marking a commit (e.g. a release). |
| tag | DCB | Domain metadata on an event for partitioning / consistency queries (e.g. product:123). |
| tag | GES | Three notions that do not map onto each other. GES’s tag is a core record field (§6.2, §18b), a key:value label: the selector the core query reads (§10.0) and the basis of tag search (§16b). It is not a Git tag (a named ref) and has no Emmett equivalent — in Emmett the same information is a plain data/metadata field, and the Emmett binding surfaces no tags. |
| snapshot | Git | Every commit’s tree is a full snapshot of the tree state. |
| snapshot | Event sourcing / RDBMS | A materialized fold of state at a position, kept as a read optimization. |
| snapshot | GES | Reserved for the §13 lossless state snapshot (a fold result under refs/ges/<store>/snapshots/*; events kept). The shadow working-tree artifact is a capture (§3.4); a remote-tracking copy is a point-in-time copy (§12) — neither is called a snapshot. |
| leader | EventStoreDB / Axon / consensus | A consensus-elected node coordinating writes, with quorum and failover. |
| sequencer | Shared-log (Corfu) | A dedicated component that assigns global-stream positions. |
| authority | GES | Neither of the above: an authoritative remote — a role assigned to a remote by convention (default name origin), no election, no dedicated node (§20.5). |
| origin | Git | The conventional default name of the primary remote. |
| origin | GES | The default name of the authoritative remote (§20.5). |
| writer / appender | Event sourcing / Axon (EventAppender) |
The actor that appends events to the store. |
| writer / appender | GES | A process calling appendToStream on its local store; local-first; multiple permitted; never the ordering role (§2). |
| decider | Functional ES / Emmett | The pure function (command, state) → events; “command handler” in Axon. |
| decider | GES | The same decision role; GES stores the events it emits but does not constrain how deciding is done. |
| ref | Git | A named mutable pointer to a commit. |
| ref | GES | The visible pointer whose CAS-guarded update is the transactional boundary (§5.5); a stream’s tip. |
| projection | Event sourcing | A read model built by folding events. |
| projection | GES | §16c read model; its cursor is a ref, and it is always local (never pushed, §20.5.3). |
| merge | Git | Combine divergent branches; may produce textual conflicts. |
| merge | GES | Union of distinct streams (conflict-free); same-stream divergence is a rejected concurrency conflict, never auto-merged (§12.5). |
| chain | Git | Commit ancestry; being content-addressed it is inherently a Merkle chain. |
| chain | GES | Two distinct things, named apart: commit chain (the Git ancestry / Merkle chain) vs integrity chain (the in-record predecessorHash links, §8.3). “hash chain” is not used bare. |
| index | Git | The staging area (.git/index; a temporary index for shadow capture, §3.4). |
| index | GES | A derived lookup index (tag index, §18b; read-model index, §16c) — always qualified; never bare “index” for a GES structure. |
| canonical | GES | Reserved for canonical JSON (JCS, §6.1). The remote is the authoritative remote (not “canonical”); real refs are live refs (not “canonical”). |
| log | Event sourcing / streaming | Loose: “event log” ≈ the store or the append-only nature; the streaming word (Kafka commit log). Collides with general-software “event log” (diagnostic logging). |
| log | GES | Not a GES term for the store. The store is the event store; the records are the events / canonical JSON records; the the global stream primary chain is the global stream. “log” appears only in git log (the command) and when quoting other systems (Raft log, ESDB $all). |
| id | GES | The record field id is the event id (§8.2). Otherwise always qualified: stream name, state id, worktree id, append id. Never a bare unqualified “id” for anything but the event id. |
| shared | GES | Three senses, disambiguated by phrase: agreed among checkouts (“shared source of truth”, “single shared remote”); replicated / at a remote (the §12.2 status shared); structural sharing (one blob referenced by many). “Pushed” is preferred over “shared” for the replicated sense in prose. |
| write | GES | The operation is append (appendToStream); “write” survives only in the established compounds write model (CQRS) and write path (the durable I/O path, §10.4). |
| multi-master | GES | Description of the substrate topology (§3–§15): every clone is an authority over its own visibility point; remotes are peers unless a role is assigned by convention. |
| global stream | GES | The mandatory primary ordering (§20, C22): the totally-ordered commit chain over all events of one store; one authoritative remote is designated sequencer by convention; state streams are secondary views. “Global” is store-wide, never universe-wide (§20.1.1); how many stores a repository hosts is an open axis (§20.1.2). A special case of the multi-master topology with an agreed winner for global-stream races. |
| bounded context | DDD | A boundary within which one model and its ubiquitous language stay internally consistent (Evans, 2003). |
| bounded context | GES | Two uses, deliberately distinguished. Linguistic: the vocabulary zones of this table and the spec modules of ADR-001 — zones of this specification’s own language. Deployment: the domain boundary one GES store serves (§20.1.1) — not, as such, one repository (§20.1.2). The second sense is what scopes the word “global”; where this document says “bounded context” without qualification it means the second. |
3. Repository layout
Section titled “3. Repository layout”3.1. A GES store MAY share a repository with source code, or occupy a dedicated repository. When shared, events and code MAY be committed together or on separate refs; §3.4 governs both.
3.1.1. A repository MAY host more than one store. Every GES ref therefore carries a store segment naming the store it belongs to, so that two bounded contexts (§20.1.1) can occupy one repository without colliding. The segment is MANDATORY, including where a repository hosts exactly one store: one layout, no special case. A store name is arbitrary UTF-8 encoded into a ref component by the same reversible rule as a stream name (§4), and it SHOULD be the name of the bounded context the store serves.
3.2. Every ref of a store MUST live under that store’s segment, in this layout:
refs/ges/<store>/global the global stream (§20)refs/ges/<store>/streams/<encoded-stream-name> state streamsrefs/ges/<store>/tags/<encoded-tag> tag indices (§18b.3)refs/ges/<store>/snapshots/<encoded-stream-name> snapshots (§13)Implementations MUST NOT store event streams under refs/heads/*. GES refs live outside the branch namespace so that ordinary branch operations, GC reachability, and CI tooling do not interfere with them, and vice versa.
3.2.1. Each state stream MUST map to exactly one ref under streams/. The global stream (§20, C22) MUST be the single ref global, and each tag index (§18b.3) MUST be one ref under tags/.
3.3. Snapshots (§13) MUST be stored under that store’s snapshots/ as shown above; which position a snapshot folds up to is a field of its record, not part of its ref name.
3.3.1. Shadow-capture refs (§3.4) are local-only and MUST live under:
refs/ges/<store>/shadow/<HEAD7>-<worktree-id>These refs MUST NOT be pushed to any remote (they are scratch); only the condensed events on refs/ges/<store>/streams/* are pushed to remotes.
3.5. The reserved prefix refs/ges/ MUST NOT be used for any purpose other than the layout of §3.2, the local-only refs of §3.3.1, and the refs an extension the implementation claims defines under a store segment.
3.6. Ref-storage backend (stream fan-out). One ref per stream means a store with many streams has many refs. Under the default loose-ref backend, each ref is a separate file, so a large stream count reproduces the “many files in one directory” enumeration problem — this time for refs, not events. Implementations targeting many streams MUST mitigate this by using either packed refs (git pack-refs --all, a single packed-refs file) or, preferably, the reftable backend (designed for repositories with very large ref counts). Implementations MUST NOT rely on loose-ref enumeration performance as stream count grows, and MUST read a single stream’s tip by direct ref resolution, never by listing the namespace.
4. Stream naming and ref encoding
Section titled “4. Stream naming and ref encoding”4.1. streamName is an arbitrary non-empty UTF-8 string. Git ref names forbid many characters (see git check-ref-format). Therefore the implementation MUST encode streamName into <encoded-stream-name> with a reversible transform that yields a valid ref component.
4.2. The encoding MUST:
- be deterministic and reversible;
- preserve readability for the common case of
[A-Za-z0-9._-]and/used as a hierarchical separator; - percent-encode (
%XX, uppercase hex, UTF-8 bytes) every character that is not in the unreserved setA–Z a–z 0–9 - . _and is not a single internal/; - never produce a component that is empty, ends in
.lock, contains.., or begins or ends with/.
4.3. The raw, unencoded streamName MUST also be stored inside every event record (§6.2) so that stream identity round-trips independently of ref encoding.
5. Event storage granularity (normative)
Section titled “5. Event storage granularity (normative)”5.1. Events MUST be stored as whole, self-contained records, one file per event. An event file MUST contain the complete fact and MUST NOT be stored as a diff, delta, or increment against any prior state.
Rationale: an event is an absolute fact, not a change relative to a parent. Storing diffs conflates the events with their projection, makes an event’s meaning depend on lineage, and breaks cherry-pick, reorder, and cross-stream merge. Git’s packfiles already delta-compress similar blobs transparently, so storing each event whole costs no more storage than a hand-rolled diff scheme.
5.2. One commit MUST correspond to exactly one event. Each commit’s tree adds exactly that one event file (delta-tree). This maximizes auditability: git log is one line per event, git show <sha> is exactly one event, and the event↔commit mapping is 1:1. Reading a stream is a commit walk (§10.2), one event per commit.
5.2.1. Atomic multi-event appends. Emmett’s appendToStream accepts an array and MUST be atomic. With commit = one event, an append of N events MUST be realized as a chain of N commits built on the current tip (oldTip ← e1 ← e2 ← … ← eN), made visible by a single atomic compare-and-swap of the stream ref oldTip → eN (§9.2). The intermediate commits are unreferenced until that ref update: a crash before it leaves only unreferenced objects (GC garbage) and the stream unchanged; a successful ref update makes all N reachable at once. Atomicity therefore rests on the ref move, not on any single commit. Implementations MUST NOT advance the ref incrementally across the events of one append.
5.2.2. Consequence — the commit chain already hash-chains events. Because each commit has exactly one event and one parent, the commit ancestry is itself a per-stream, per-event Merkle chain (§8.1); the in-record predecessorHash (the integrity chain, §8.3) then serves only export-hardening, not ordering.
5.3. Ordering is the commit ancestry, corroborated by the in-record streamPosition (§6.2); it is never derived from directory listing. Each commit’s single event file MUST be named for readability and grep:
<20-digit-zero-padded-streamPosition>-<Type>.jsonExample: 00000000000000000042-OrderPlaced.json. One file per commit tree means no directory ever accumulates entries; the filename is a convenience, the authoritative order is the ancestry plus streamPosition. Implementations MUST NOT depend on flat-directory enumeration to establish order. (An “event file” here is a blob at a tree path — a Git object — not a working-tree file; see §6.3.5.)
5.4. Directory fan-out for accumulating layouts. The delta-tree model keeps each commit tree small, so no fan-out is required there. However, any layout that collects many event files under a single tree — an accumulating whole-stream tree, a materialized lookup index, a content-addressed global event pool, or an implementation that deliberately chooses accumulation over delta-trees — MUST shard files into a prefix-tree (trie) of subdirectories rather than a flat directory, because (a) a Git tree object with very many entries must be fully parsed to read or modify any entry, and (b) filesystem directory enumeration degrades with entry count. The fan-out MUST mirror Git’s own object store (objects/ab/cdef…):
- Order-preserving (event files): shard on the zero-padded
streamPosition, high-order segments first, e.g.0000/0000/0000/0000/0042.json(fixed-width segments). This keeps lexical order equal to positional order and makes range reads (readStream(from,to)) map to a bounded set of subtrees; the tail subtree is the only write hotspot, which is acceptable for an append-only stream. - Content-addressed (id-keyed pools, Entire-style): shard on the hex event id, e.g.
a3/b2/c4d5….json, for uniform distribution; order is then recovered solely from the in-recordstreamPosition, never from listing.
The segment width and depth are implementation-defined but MUST be documented and MUST keep any single directory below an implementation-stated bound (RECOMMENDED ≤ 4096 entries).
Figure (informative): Atomic multi-event append and transactional boundary. An append of three events (e1, e2, e3) builds a chain of three one-event commits in memory on the current tip, then makes them all visible via a single update-ref CAS. The ref move is the transactional point: before it, the commits are unreferenced garbage; at the CAS, all three become reachable atomically; a crash before the CAS leaves the stream unchanged.
gitGraph LR: commit id: "prior" tag: "stream-v0" commit id: "e1" commit id: "e2" commit id: "e3" tag: "stream-v3"Caption: After the update-ref CAS succeeds (moving the stream ref from prior to e3), all three events are visible at stream positions 1, 2, 3. A crash before the CAS leaves all three as unreferenced objects and the stream tip unchanged.
5.5. Transactional boundary (the ref update is the commit point). Writing a Git commit object only places an unreferenced object in the store — it is not yet visible and not yet in effect. The write becomes visible and atomic at the ref update (git update-ref, or git update-ref --stdin across the several refs of a dual-view append, §20.4), which is compare-and-swap-guarded (§9.2). That ref update — not the commit object — is the transactional boundary: before it, nothing of the append is reachable; at it, all of the append’s commits become reachable at once; a crash before it leaves only GC garbage. Compare-and-swap on the ref is the store’s optimistic-concurrency check (the losing writer is rejected and retries, §9.5).
5.5.1. Two properties that a single database COMMIT bundles are, in GES, deliberately decoupled from this visibility point and MUST NOT be assumed to come with it: durability (the objects and the ref are durable only if fsync is configured, §11 — “visible” ≠ “durable”), and authoritative/global visibility (a local ref update is visible only in the local repository; the append becomes globally authoritative only when fast-forward-pushed to the authoritative remote, §20.5).
5.5.2. Non-normative analogy. Relative to PostgreSQL: writing the commit object(s) is like the un-committed row writes of a transaction; update-ref is the COMMIT (the atomic, CAS-guarded moment the write becomes visible); but unlike Postgres — where COMMIT also guarantees durability (WAL fsync) and immediate server-wide visibility — GES splits those into two further, separately-controlled steps (fsync durability §11; push-for-authority §20.5). GES also provides no per-reader MVCC snapshot: a reader SHOULD resolve a tip once and read from that fixed commit, since a concurrent ref update can move the tip mid-read.
6. Event record format
Section titled “6. Event record format”6.1 Canonical JSON
Section titled “6.1 Canonical JSON”6.1.1. All hashing, event-id derivation, and integrity chaining (§8) operate over a canonical serialization. Implementations MUST use RFC 8785 (JSON Canonicalization Scheme, JCS): UTF-8, lexicographically sorted object keys, no insignificant whitespace, and the RFC 8785 number and string rules.
6.1.2. The stored file MAY be pretty-printed for readability, but every hash MUST be computed over the JCS form, not the pretty form. Implementations SHOULD store the pretty form (2-space indent) to maximize diff and agent readability, and recompute JCS on demand.
6.2 Record schema
Section titled “6.2 Record schema”Each event file MUST be a JSON object with these fields:
| Field | Type | Req. | Meaning |
|---|---|---|---|
gesVersion |
string | MUST | Format version, "1" for this spec. |
streamName |
string | MUST | Raw stream name (§4.3). |
streamPosition |
string | MUST | 1-based sequence number (the event’s stream position), encoded as a decimal string (values exceed Number.MAX_SAFE_INTEGER; maps to Emmett BigIntStreamPosition). |
id |
string | MUST | Event id (§8.2). |
type |
string | MUST | Emmett event.type. |
data |
object | MUST | Emmett event.data (the domain payload). |
metadata |
object | MUST | Emmett writer event.metadata (e.g. correlationId, causationId); MAY be {}. |
timestamp |
string | MUST | RFC 3339 UTC instant of the append, recorded in-band (do not rely on the mutable committer date). |
predecessorHash |
string | null | SHOULD | Integrity chain link (§8.3); null for the first event of the stream. |
tags |
string[] | MUST | Zero or more key:value tags (§18b), including the state-stream tag of §18b.4. Authoritative here; the selector the core query reads (§10.0) and the basis of tag search (§16b). MAY be empty apart from the state-stream tag. |
6.2.1. Writer metadata MUST be stored inside the event file, not in the commit message, because it is part of the immutable fact and MAY be structured/nested. Commit messages are mutable via rebase and are unsuitable as the source of truth for any immutable field.
6.2.2. Recorded fields that Git already provides (author, commit time) MUST NOT be treated as authoritative event data; the authoritative timestamp and identity live in the record (timestamp, id).
6.2.3. When tags are present, the commit SHOULD also carry them as denormalized GES-Tag: <key:value> trailers (§7.3.2) so git log --grep can select by tag without opening trees. The record remains authoritative.
6.3 Serialization and storage efficiency
Section titled “6.3 Serialization and storage efficiency”6.3.1. JSON is retained; readability is a first-class goal (§2). Records MUST NOT be stored in a non-human-readable encoding in the event store. Binary encodings (MessagePack, CBOR) and schema encodings (Protobuf, Avro) are NON-CONFORMING for the event store because they break git show, git grep, and agent cat, and — for schemaless binary — still repeat map keys per record without beating general compression. An implementation MAY offer a clearly-labeled OPTIONAL binary encoding for adopters that explicitly forgo readability, but it MUST NOT be the default and MUST be documented as sacrificing §2.
6.3.2. Repeated keys/values are compressed by Git, not by the format. Loose objects are individually zlib-deflated; after git gc/git repack, Git delta-compresses similar objects, so events that share most keys and values are stored as a base plus small deltas, and cross-event repetition is largely eliminated. Because the canonical form sorts keys (§6.1), records are byte-similar and delta well. Implementations and adopters MUST measure packed size (post-gc) rather than loose-object size when reasoning about storage, and SHOULD run git repack/git commit-graph write as maintenance (§11.6).
6.3.2.1. Packing bounds the rate of growth, not the total. Delta compression removes redundancy but not accumulation: an append-only stream grows monotonically by at least the compressed entropy of each event body plus per-object overhead, forever. Binary/schema encodings do not change this — they only shrink the already-cheap repeated fraction and leave the expensive parts (body entropy, commit/tree metadata, signatures) untouched, which is a further reason they are not worth the readability loss (§6.3.1). The mechanism that bounds total on-disk size is compaction and truncation (§13, §13.5), not packing. Implementations MUST NOT present packing as a substitute for a retention policy.
6.3.3. Record-schema minimization (optional, for cleanliness not bytes). Fields that Git derives — notably gesVersion (constant per store/stream, MAY be moved to a per-stream header object at the stream root), and optionally streamPosition/id (derivable from ancestry / content hash) — MAY be omitted from per-event records. Implementations SHOULD treat this as a cleanliness choice, not a storage optimization, since delta packing already collapses the repetition; streamName in particular is retained per §4.3 for round-trip self-containment.
6.3.4. Analytical efficiency is a read-model concern. Dictionary-encoded, columnar, repetition-eliminating storage (Parquet, ORC, DuckDB) is batch-oriented and unreadable and is therefore appropriate only as a derived, disposable projection (§16c), never as the source of truth. Implementations needing fast analytical queries SHOULD project the events into such a store and rebuild it by replay, keeping the canonical JSON records authoritative.
6.3.5 Physical substrate: Git objects, not working-tree files
Section titled “6.3.5 Physical substrate: Git objects, not working-tree files”6.3.5.1. Throughout this spec, an “event file” is a blob at a path inside a Git tree — a Git object — not a .json file in a working directory. In the no-checkout model (§10.1) events exist only in the object store (.git/objects); they are materialized as OS files only if explicitly checked out, which conforming stores do not do by default. Agent/human readability (§2) is served by reading blobs on demand (git show <ref>:<path>, git cat-file), which returns the plain JSON without a checkout.
6.3.5.2. Loose vs packed. A newly written object is a loose object: one OS file, individually zlib-deflated, occupying at least one filesystem block (typically 4 KiB) plus an inode. A burst of many small events therefore incurs real per-file waste and can exhaust inodes — the classic “many small files” cost. Git’s purpose-built remedy is the packfile: git gc/git repack consolidates many objects into a few large files with zlib and cross-object delta compression, eliminating both the per-file filesystem overhead and the redundancy. Append-heavy stores SHOULD pack proactively (tune gc.auto, or repack on a schedule) rather than let loose objects accumulate; implementations MUST NOT reason about disk cost from the loose-object state.
6.3.5.3. Do not pre-compress blobs. Events destined for the object store MUST NOT be pre-compressed at the record level: Git already compresses blobs, and pre-compression defeats cross-event delta compression and makes the content opaque to git grep and to agents — losing both readability and ratio.
6.3.5.4. Working-tree materialization is a different design. An implementation MAY instead keep events as literal files in a working directory (e.g. for out-of-band browsing). It then owns the small-files cost directly, and the standard mitigation is segmentation — one file per stream/segment holding many events (e.g. JSONL) — which amortizes filesystem overhead and enables whole-file compression, at the cost of reintroducing append/line-level merge conflicts (§5.1 rationale). The Git-object substrate avoids this trade because packing consolidates without altering the one-event-per-commit structure. Implementations choosing working-tree materialization MUST document the merge and storage consequences.
7. Commit model
Section titled “7. Commit model”7.1. The commit tree MUST contain exactly one event file (§5.2).
7.2. The commit message carries denormalized index data — a denormalized copy, never the source of truth. The first line MUST be a human-readable summary:
<streamName>: <Type> @<streamPosition>7.3. The message MUST include these trailers (RFC-822-style Key: value, parseable by git interpret-trailers), enabling an O(1) version read from the tip commit without opening the tree:
GES-Version: 1GES-Stream: <raw streamName>GES-Position: <streamPosition of this event>GES-Sequence: <sequence position of this event>7.3.1. GES-Sequence carries the event’s sequence position (§20) — the position an append condition’s after names (§9.1.1) — so that evaluating a condition does not require counting commit depth. It is provisional until the event is sequenced (§20.3.2) and MUST be rewritten when a rebase reassigns it (§20.7.1). The stream version is the GES-Position of the tip commit. A multi-event append advances the tip by N commits (§5.2.1); the version after such an append is the GES-Position of the last event, and stream version derivation MUST use the trailer of the tip, not commit counting from an assumed 1:1 append↔commit mapping. Implementations MAY additionally record GES-Append-Id: <id> on every commit of one append so the events of a single atomic append are groupable after the fact.
7.3.2. Optional trailers: GES-Correlation: <id> (denormalized grep index); GES-Link: <ref> — the linkage between a code commit and the append it justifies (§3.4.3). It MAY appear on the code commit naming the append id, and/or on the event commits naming the code-commit SHA/URL; both directions are resolvable by trailer grep.
7.4. Commits on stream refs SHOULD be signed (git commit -S, GPG or SSH signature) so authorship and integrity are cryptographically verifiable, not merely asserted (§8, §15).
8. Identity and integrity
Section titled “8. Identity and integrity”8.1 Git is already a Merkle chain
Section titled “8.1 Git is already a Merkle chain”Each commit references its parent and its tree hash, which recursively hashes all blobs. The chain of stream-ref tips (each tip’s parent is the previous tip) is therefore already a per-stream Merkle chain (a commit chain that recursively hashes its history). Implementations MUST NOT duplicate this with a redundant in-commit “hash of previous commit” field.
8.2 Event id
Section titled “8.2 Event id”8.2.1. id MUST be stable and content-derived unless supplied by the caller. When derived, it MUST be:
id = "sha256:" + hex( SHA-256( JCS(record without `id` and `predecessorHash`) ) )8.2.2. Idempotency: appending an event whose derived id already exists at the same streamPosition MUST be a no-op (deduplicated), not a duplicate.
8.3 Application-level integrity chain (export-hardening)
Section titled “8.3 Application-level integrity chain (export-hardening)”8.3.1. Implementations SHOULD populate predecessorHash:
predecessorHash(e_n) = "sha256:" + hex( SHA-256( JCS(e_{n-1}) ) )predecessorHash(e_1) = nullwhere e_{n-1} is the immediately preceding event in the same stream.
8.3.2. The chain MUST be per-stream, never global. A per-stream chain is well-defined because each stream is single-writer-at-a-time; a global chain would fork under branching (two branches appending after the same tip) and is incompatible with Git’s DAG. A global in-record chain would also be rewritten by global-stream reconciliation (§20.6–§20.7); the global stream’s ordering integrity comes from its commit ancestry, not from an in-record chain.
8.3.3. Algorithm choice. The application chain MUST use SHA-256. Git’s own object hashing MAY remain SHA-1 (with collision detection) for hosting interoperability; integrity claims a party defends externally MUST NOT rest on SHA-1.
8.4 The chain is necessary, not sufficient
Section titled “8.4 The chain is necessary, not sufficient”8.4.1. An integrity chain alone (Git’s Merkle ancestry or the §8.3 predecessorHash links) does not make the event store tamper-proof: an attacker who can rewrite history recomputes the whole chain and it stays internally consistent (this is exactly what git rebase/filter-branch do). Tamper-evidence requires an external anchor plus append-only enforcement:
- signed tips or signed tags (§7.4) whose expected value is known to verifiers;
- server-side append-only enforcement (§15).
8.4.2. Reproducibility strengthens auditability beyond readability: because the decider is pure and events are the only inputs, a verifier MUST be able to replay a stream and confirm the derived state follows. “Readable + deterministic” = “recomputable”, a stronger property than “inspectable”.
9. Concurrency and the append condition
Section titled “9. Concurrency and the append condition”9.1 Two distinct notions
Section titled “9.1 Two distinct notions”9.1.1. Contract (append condition). The core write precondition is the DCB append condition (§18c.1): a query plus an optional after sequence position. The condition itself is optional too (§9.3.1). Where a condition is present and names after, the append MUST fail if any event matching the query exists after that position; where it is present and after is absent, the append MUST fail if any event matches the query at all. A present condition is the sole authorization of a write (§10.0); an append carrying none is authorized by its caller, and the ref transaction then protects it against a lost update and nothing more.
9.1.2. Mechanism (ref transaction). The condition is enforced by Git’s atomic multi-ref transaction (§20.4.1): the append verifies every affected ref at the tip observed when the state was read, and advances them all in one all-or-nothing git update-ref --stdin. A ref SHA is identity, not a position: it answers “is this ref exactly what I read?” and nothing more.
9.1.3. Implementations MUST separate these. The condition is what the caller reasons about; the ref transaction is what makes it indivisible. Neither substitutes for the other — a ref transaction alone cannot express “no event matches this query”, and a query alone cannot exclude a concurrent writer.
9.2 Evaluating the condition (no lost updates)
Section titled “9.2 Evaluating the condition (no lost updates)”9.2.1. To evaluate an append condition, the implementation MUST test every event after after against the query, and MUST perform that evaluation and the ref transaction as one indivisible step — either through the transaction’s per-ref preconditions, or under the serialization of the appending process (§20.3.1).
9.2.2. The evaluation MAY run against a tag index (§20.2) instead of a walk of the global stream, provided that index is advanced inside the same transaction as the append (§20.4.1). An index that can lag the global stream MUST NOT be used to evaluate an append condition.
9.2.3. If the underlying library cannot perform an atomic conditional multi-ref update, the implementation MUST fall back to a mechanism that does (e.g. invoking git update-ref --stdin, which is atomic and conditional) or an equivalent lock. A read-then-write without a guard is non-conforming.
9.2.4. Cost. A condition over a query costs a scan of the events after after, where the stream model’s expected-version check cost one ref read. This is the price of the wider boundary; §9.2.2 is the mitigation, and implementations SHOULD document the evaluation strategy they chose.
9.3 Unconditional and batch appends
Section titled “9.3 Unconditional and batch appends”9.3.1. An append carrying no condition is unconditional: it MUST still perform the ref transaction, so that no write is lost, but it imposes no query precondition.
9.3.2. With commit = one event (§5.2), a multi-event append advances each affected ref by N commits atomically (§5.2.1). The condition is evaluated once, against the state observed before the first of those events; implementations MUST NOT re-evaluate it between the events of one append.
9.4 Append-only is a precondition of coherence
Section titled “9.4 Append-only is a precondition of coherence”9.4.1. Sequence positions are only well-defined under append-only enforcement (§15). A rebase reassigns positions (§20.7.1) and produces new SHAs for the same logical events, which invalidates every stored after token and the §8.3 chain. Immutability enforcement is therefore not merely audit hygiene; it is what makes the position model coherent. Within the sequencing window a locally assigned position is provisional (§20.3.2), so an after token MUST NOT be treated as stable across a rejected push.
10. Core interface
Section titled “10. Core interface”GES’s core is framework-neutral and DCB-based — its foundational model is the one published at dcb.events: one totally-ordered event sequence (the global stream, §20), events carrying tags, selection by query, and a query-based append condition. The substrate (§3–§9), integrity (§8), durability (§11), and distribution (§12, §20) requirements are what a store must satisfy and are independent of which framework, if any, sits on top.
10.0 Core operations (append / read)
Section titled “10.0 Core operations (append / read)”The core is two operations, plus a fold for state:
append(events, condition?)— atomically persist one or more events (§5.2) under an optional append condition (§18c.1): a query that MUST match no event after a given sequence position. The condition is the sole write authorization (§9.2); there is no stream argument.read(query, options?)— replay the events a query selects, in global order, each paired with its sequence position — a sequenced event (§2.1).- plus fold to derive state from a query’s result, and snapshots (§13) to accelerate it.
- subscribe / catch-up — follow new events from a sequence position. The notification mechanism is TBD (hook-triggered, polled, or blocking — deliberately unspecified pre-implementation). The behavioural guarantee is fixed: when events are appended, projections whose query matches them are updated via cursor-based catch-up (§16c).
The stream view is derived, and it is mandatory. A state stream is the events carrying one state id, maintained as a secondary commit chain under refs/ges/<store>/streams/<encoded> (§3, §20.2) so that folding one entity’s history walks a short chain instead of filtering the global stream. It is a materialized query: a read of the query selecting that state id and a walk of its state stream MUST return the same events in the same order. Every conforming store maintains it (C22) — it is a performance view carrying a conformance obligation, not a second write path.
appendToStream is a binding over the core, not the core. A stream-based append (streamName, events, expectedStreamVersion) MUST be expressed as append(events, condition), where the query selects streamName and after is the sequence position of that stream’s event at expectedStreamVersion. The reverse mapping does not exist: an append condition whose query spans two state ids has no stream-based equivalent, and that is exactly the capability the DCB core adds over the stream model. Emmett (§10.1, extensions/emmett-binding.md) is the reference binding of this derived stream view, not of the core.
Where the analogy breaks — do not conflate three different “tag” notions. A DCB tag (domain metadata that scopes a consistency query, e.g. product:123) is not a Git tag (a named ref to a commit), and has no Emmett equivalent — in Emmett the same information is just a field in the event’s data/metadata. In GES, tags are a core record field (§6.2, §18b): the selector the core query reads (§10.0) and the basis of tag search (§16b), stored authoritatively in the record and denormalized as GES-Tag trailers. They are not Git tags. Because Emmett does not implement DCB, the Emmett binding populates and surfaces no tags; a consumer that needs them uses the core interface directly, or another DCB-aware framework, never Emmett.
Informed commands (decision loop). State read from any source — typically a read model (§16c) — MAY inform a command, but never authorizes it: the decider folds write-model state from a core read (§10.0) and the append’s append condition (§18c.1) is the sole authorization. A stale informer therefore costs at most a rejected append and a re-decide against fresh state (§9.5); it can never corrupt a stream.
The Emmett binding (§10.1–§10.3). Emmett is GES’s first and reference framework binding — a consumer of the derived stream view (§10.0), one of several possible (Axon-style command handlers or direct use bind the same stream view under their own names; a DCB-aware consumer uses the core interface directly). The methods in §10.1–§10.3 map Emmett’s EventStore names onto that stream view; the binding MUST implement Emmett’s EventStore interface, and the signatures are the target shape and MUST be reconciled with the installed Emmett version. Emmett is purely stream-based and does not implement DCB, so it reaches only the subset of the core expressible as a single-state-id append condition. Emmett compatibility is not required for GES conformance (§16.1) — a store satisfying the core tests is conformant whether or not it exposes an Emmett interface.
10.4 Library and the durable-write path
Section titled “10.4 Library and the durable-write path”10.4.1. A pure-JS Git implementation (e.g. isomorphic-git) is attractive for portability (Node and browser, no native build) and is acceptable for the read path and for non-durable/local-first stores. nodegit/libgit2 is an alternative where atomic conditional ref updates are available. Whichever is chosen, §9.2.2 governs the CAS.
10.4.2. Durability caveat (normative). A naive pure-JS writer that uses fs.writeFile writes only to the page cache: it fsyncs neither the object files, nor the containing directory, nor the ref, and on macOS a plain fsync does not flush the storage controller (F_FULLFSYNC is required). Such a writer is fast but not durable, and a store built on it MUST NOT claim local durability (§11). An implementation that claims local durability MUST either (a) delegate the durable append (object writes + ref update) to the git binary or libgit2, which implement the mature durable path (loose-object fsync, parent-directory fsync, lockfile-plus-rename ref update with sync-before-rename, batch fsync, and platform-correct flushing), or (b) replicate that full path itself, including directory and ref-file fsync. This is the single sharpest instance of the project’s core rationale: the durable-write path is exactly the battle-tested substrate that MUST NOT be hand-rolled.
10.5 Error semantics
Section titled “10.5 Error semantics”10.5.1. On a compare-and-swap failure (§9.2), appendToStream MUST surface a distinct concurrency/optimistic-lock error mapped to Emmett’s expected error type, distinguishable from I/O or validation errors. It MUST NOT silently retry in a way that could re-run the caller’s decision against stale state; retry, if any, MUST re-read the stream and re-run the decider (see §9.5).
10.5.2. Reading a non-existent stream MUST return an empty result, not an error (§10.2). A zero-event appendToStream MUST be a no-op that does not create a commit or move the ref.
9.5 Conflict handling (referenced by §10.5)
Section titled “9.5 Conflict handling (referenced by §10.5)”9.5.1. A CAS failure is a signal of a real concurrency event, not a fault. The implementation MUST treat the losing append as rejected and SHOULD expose a policy: automatic retry-with-rebase (re-read tip, re-run the decider against current state, re-attempt) for commutative/idempotent commands, or escalation to the caller for genuine business conflicts. The implementation MUST NOT apply an append built against a stale tip.
10.6 Storage-core seam (implementation-swappable)
Section titled “10.6 Storage-core seam (implementation-swappable)”10.6.1. The implementation MUST confine all Git-object, ref, and filesystem operations behind a narrow internal interface (the “storage core”), so that the Emmett binding, JSONL (§16d), projections (§16c), and tagging (§18b) depend only on that interface and never on the Git mechanism directly. The core’s surface is small: append events as a one-event-per-commit chain plus a terminal tip CAS; walk/read a stream; resolve a stream’s tip and stream version; export a range; and acquire a coarse maintenance lock (§10.6.4).
10.6.2. Rationale. This makes the language/library choice (pure-JS, git binary, libgit2, or a native/Rust core such as gitoxide) a swap behind the seam rather than a rewrite, and it is the single place where the FFI boundary — if any — is drawn coarsely (one call per whole operation, never per event).
10.6.3. Process-invocation cost. Where the core shells out to git, it MUST NOT open a shell (execFile/spawn, never exec), and MUST NOT fork one git process per event on a hot path. For bulk writes it SHOULD use a single long-lived importer (git fast-import) and for bulk reads/export a single long-lived git cat-file --batch (§11.6, §16d.2). An in-process library (libgit2/gitoxide) is an acceptable alternative that avoids process invocation entirely.
10.6.4. Locking. The append-path guard MUST be the ref CAS (§9.2), delegated to git update-ref <ref> <new> <old> (Git’s lockfile-plus-rename protocol) rather than a hand-rolled advisory lock; implementations relying on this MUST handle stale *.lock files left by a crashed writer. Coarse-grained cross-process critical sections that are not reducible to a single ref update — compaction/maintenance runners, a singleton indexer/exporter — MUST be guarded by an explicit maintenance lock with ownership and staleness semantics (e.g. a PID/heartbeat lock in the manner of Git’s own gc.pid, or a directory/mkdir lock as used by proper-lockfile, since Node’s built-in fs lacks first-class advisory locking). This maintenance-lock primitive is part of the storage-core seam and MAY be provided by a native implementation where stronger OS locking (flock/fcntl) is required.
10.6.5. Selection guidance (non-normative): prefer execFile/spawn over exec; on measured fork pressure, move to fast-import/cat-file --batch; if still insufficient, an in-process library; a native/Rust core last, justified by a measured CPU hotspot after fsync is handled (§11.4) or by the desire for a single portable/embeddable core. Do not adopt a native core to fix the fsync-bound write path — language does not reduce fsync latency.
11. Durability, fsync, and the four write guarantees
Section titled “11. Durability, fsync, and the four write guarantees”The bottleneck of an event store is fsync latency, not compute. The write path is dominated by the cost of making data durable, not by serialization or the fold. Every design choice below is oriented around minimizing and correctly configuring durable writes.
11.1 The four guarantees
Section titled “11.1 The four guarantees”“Guaranteed writes” decomposes into four independent properties. GES’s posture:
| Property | GES posture |
|---|---|
| Atomicity (whole-or-nothing append) | Guaranteed, free. Ref update is atomic; a crash mid-append leaves only unreferenced objects (GC-collected garbage), never a half-visible append. |
| Isolation / no lost update | Guaranteed via §9.2 conditional ref CAS. |
| Local durability (survives crash) | Configurable, NOT the default — the default config actively omits it. See §11.2. |
| Replicated durability / HA | Not provided (N1). See §12.4 for an opt-in per-stream mechanism. |
11.2b Durability levels (tunable, like mature event stores)
Section titled “11.2b Durability levels (tunable, like mature event stores)”Durability is a spectrum, not a switch. An implementation MUST support and label at least one of these levels and MUST state which is in effect per store (and MAY vary it per stream):
- L0 — none. No fsync; rely on OS writeback. Fastest, unbounded loss window on crash. Equivalent to a page-cache-only writer (§10.4.2). MUST be labeled non-durable.
- L1 — windowed / async. fsync periodically (every
Tms or everyNappends). An append MAY become visible (ref moved) before it is durable, so a crash loses at most the appends within the current window. The boundT/NMUST be documented. This is the explicit throughput-vs-loss trade offered by stores like PostgreSQL async commit; the loss window is a stated guarantee, not an accident. - L2 — synchronous. fsync objects and ref before acknowledging each append (§10.1 durability ordering). No loss window. RECOMMENDED default.
11.2b.1. At low event frequency, L2 is effectively free (sync latency ≪ inter-append interval) and SHOULD be preferred for its simplicity and zero loss window. L1 is worthwhile only when fsync_latency × append_rate approaches saturation (§11.4).
11.2b.2. Under L1, the visible-but-not-yet-durable interval means a reader (or a pushed replica) MAY observe an append that a subsequent crash erases. Implementations offering L1 MUST document this and MUST NOT combine L1 with a claim of “no lost acknowledged writes.”
11.2 The default Git config is not durable for an event store
Section titled “11.2 The default Git config is not durable for an event store”11.2.1. Since Git 2.36, core.fsync/core.fsyncMethod replace the deprecated core.fsyncObjectFiles. The shipped default (objects,derived-metadata,-loose-object) explicitly excludes loose objects, so freshly written commits, trees, and blobs may be lost on unclean shutdown. Ref updates are likewise not fsynced by default. Either gap silently loses appends: without loose-object durability the events vanish; without ref durability the tip pointer fails to survive even though the objects did, leaving the append invisible.
11.2.2. An implementation at level L2 (or L1 at each window flush) MUST configure, on every repository it writes:
git config core.fsync loose-object,reference # at minimum; or: allgit config core.fsyncMethod batch # see 11.3reference is mandatory, not optional: the stream tip is the source of truth for “this append happened,” and a non-durable tip means a durable-but-orphaned append.
11.3 fsync is amortized by batching (group commit)
Section titled “11.3 fsync is amortized by batching (group commit)”11.3.1. core.fsyncMethod=batch writes out all objects added in one Git operation and then issues a single fsync (via a flush to a dummy file), instead of one fsync per object. A multi-event append (§5.2.1) writes N commits’ objects in one operation, so batching collapses their syncs toward one. Implementations at L1/L2 SHOULD enable it.
11.3.2. Group-commit generalizes this: an L1 implementation MAY accumulate several appends (to the same or different streams), write all their objects, issue one batched fsync, then perform the ref CAS updates — amortizing one sync across many appends at the cost of the window’s loss bound.
11.4 The honest throughput ceiling
Section titled “11.4 The honest throughput ceiling”11.4.1. With durability required, per-stream append throughput is bounded by roughly 1 / fsync_latency appends per second, independent of CPU. Implementations MUST NOT claim throughput that assumes compute is the limit, and SHOULD publish measured durable-append latency for their target storage. Non-durable/local-first configurations are not bound this way but MUST be labeled non-durable.
11.5. Implementations MUST state, per deployment, which durability level is in effect. Claiming L2 without core.fsync including at least loose-object,reference is non-conforming.
11.6 Performance calibration (“is git fast?”)
Section titled “11.6 Performance calibration (“is git fast?”)”11.6.1. For the event-store access pattern — append at the tail, walk a stream, fold — Git’s profile is well matched: content-addressed object I/O and DAG walks are fast, and the fold reads the stream linearly anyway. Two honest edges apply:
- Write latency is fsync (§11.4) plus, if the durable path shells out to the
gitbinary, process start (~1–5 ms per invocation). At low append frequency both are invisible. If append rate ever makes fork cost matter, a long-lived process (in-process libgit2, or a persistentgit cat-file --batch-style helper) removes it. - Git is not a query engine. It is fast for object I/O and ancestry walks, but has no secondary indexes or ad-hoc queries. This is irrelevant to event sourcing (replay, don’t query) and is precisely why the fit holds only for this pattern. For long histories,
git commit-graph writeaccelerates walks and periodicgit repackkeeps object reads fast; implementations SHOULD run both as maintenance.
12. Distribution and remotes
Section titled “12. Distribution and remotes”12.1. Every clone holds complete, independently writable copies of its stores. Reconciliation is via fetch/push/merge.
12.2. Local vs shared. Remote-tracking refs (refs/remotes/<remote>/ges/<store>/streams/*) record what was last seen at a remote. Implementations SHOULD expose the three-state status per event:
- local-only: in
refs/ges/<store>/streams/Xbut not in anyrefs/remotes/*/ges/<store>/streams/X— the outbox,<remote>/ges/streams/X..ges/streams/X. - shared: present in a remote-tracking ref (as of last fetch — not live).
- incoming: in a remote-tracking ref but not yet locally applied.
Implementations MUST configure the refspec so stream refs are fetched into the remote namespace, and MUST treat remote-tracking refs as point-in-time copies, requiring a fresh fetch for any live durability claim.
12.3. Authority model. One remote is the authoritative remote — the global-stream sequencer (§20.5); other clones are caches, and a store with no remote is trivially its own sequencer. A “guaranteed write” means “accepted by the authoritative remote” (§12.4). Clone-to-clone merging remains conflict-free only under §12.5 and never advances the global stream — that is the sequencer’s alone (§20.6).
12.4. Opt-in replicated durability (per stream). For a stream requiring N1, the implementation MAY make a successful fast-forward push to the authoritative remote part of the append: the append is acknowledged only after the push succeeds. The authoritative remote’s non-fast-forward rejection is then the distributed expectedVersion. Cost: that stream is no longer offline-writable. This trade MUST be per-stream and documented.
Figure (informative): Conflict-free union of distinct streams vs rejection of same-stream divergence. Clone A and Clone B append independently to different streams (s1, s2); on fetch/merge, both streams’ refs advance by union — conflict-free. If both clones appended to the same stream from the same tip, the second push is rejected non-fast-forward (never auto-merged); the losing writer re-reads the tip, re-runs the decider, and retries.
gitGraph LR: commit id: "c0" branch s1 commit id: "a1" commit id: "a2" checkout main branch s2 commit id: "b1" commit id: "b2"Caption: Clone A appended a1, a2 to stream s1; Clone B appended b1, b2 to stream s2. On merge, both streams’ refs are updated — union by separate refs, no conflict. The shared root c0 in the diagram is a gitGraph drawing artifact; in a real store, distinct streams are independent commit chains with no shared history. Separately: if both clones appended to the same stream from a single tip, the push that arrives second at the remote is rejected (non-fast-forward) — not merged — forcing the loser to re-read the intervening events and re-decide.
12.5. Conflict-free merge. Distinct streams are distinct refs and merge by union with no conflict. Two writers to the same stream produce competing tips for the same predecessor; this is a concurrency violation, and on push the losing writer MUST be rejected and retried — correct event-store semantics, not data loss. Implementations MUST NOT auto-merge divergent histories of a single stream.
13. Compaction, snapshots, and retention
Section titled “13. Compaction, snapshots, and retention”13.1. Two operations MUST be treated as distinct:
-
Snapshot (lossless). Fold events up to position N into a state object; keep the events. Stored under
refs/ges/<store>/snapshots/<enc>as a commit whose record contains{ gesVersion, streamName, upToPosition, state, sourceTipHash }.aggregateStreamSHOULD start from the latest snapshot and replay only events after it. Snapshots break nothing. -
Truncation (lossy). Delete events at or below a horizon and keep only a snapshot. This is a history rewrite and MUST NOT occur on live stream refs except under §13.2.
13.2. Horizon rule. Truncation MUST only remove events strictly below a replicated, causally stable horizon — a position all relevant replicas have already fetched. (Analogous to Raft log truncation after snapshot and quorum apply; unrelated to Kafka key-compaction.) After truncation, the retained snapshot MUST re-anchor to the pre-truncation tip hash so the §8.3 chain and reproducibility remain verifiable from the snapshot forward.
13.3. Erasure (GDPR Art. 17) via compaction. In an append-only, distributed, hash-chained event store, erasure of personal data is only achievable by compaction: fold/redact behind the horizon and re-anchor. Implementations handling personal data SHOULD support crypto-shredding: store event data/metadata encrypted with a per-subject key held outside the repository, so that deleting the key renders the payload unrecoverable without a history rewrite. Implementations MUST document that plaintext personal data written to a stream cannot be retroactively deleted from already-distributed clones without a coordinated rewrite.
13.4. Implementations MUST state that unbounded streams grow without a retention policy, and SHOULD provide snapshot + horizon-truncation tooling.
13.5 Storage floor and reclamation
Section titled “13.5 Storage floor and reclamation”13.5.1. The floor. On-disk cost grows monotonically. Per event the irreducible cost is approximately: the compressed entropy of the event body, plus Git commit + tree object overhead (author/committer/parent/tree/message/trailers — largely incompressible because it varies), plus, if signing per event (§7.4), an incompressible signature. Under commit = one event (§5.2), this per-event overhead is paid for every event and can dominate the body at scale. Packing does not remove it (§6.3.2.1).
13.5.2. Reclamation levers, in approximate order of impact:
- Horizon truncation (§13.2) is the only mechanism that bounds total size: fold cold events into a snapshot, delete them,
git gc. - Sign tips/snapshots/tags, not every event. Per-event signatures are pure incompressible bytes; anchoring integrity at the tip or at snapshot/tag boundaries (§8.4) preserves tamper-evidence while removing the largest avoidable per-event cost.
- Compact cold history to a batched representation. An implementation MAY keep the recent tail as granular one-event commits (audit, §5.2) while compaction rewrites cold history into fewer commits or a single snapshot — reconciling per-event auditability with storage. Hot = granular and readable; cold = compact.
- Partial/shallow clone with a promisor remote. A clone need not hold all history locally; objects are fetched on demand. This decouples “the full history is large” from “every clone pays for all of it locally,” at the cost of requiring the remote for deep reads.
- Reflog and GC hygiene. Every ref CAS writes a reflog entry; on append-heavy
refs/ges/<store>/streams/*the reflogs themselves grow. Implementations SHOULD disable or aggressively expire reflogs on GES refs and tune the GC grace so aborted/garbage appends are reclaimed promptly (exactgc.reflogExpire*/gc.pruneExpirevalues are version-dependent and MUST be verified against the target Git).
13.5.3. The sovereignty–disk trade. “Every clone is a full backup” (§12.1) means every clone bears the full history until §13.5.2’s partial-clone lever decouples it. Implementations MUST document this so adopters size storage per clone, not once.
14. Branching semantics
Section titled “14. Branching semantics”14.1. Speculative future (append hypothetical events to a tip on a throwaway branch, run the decider/projection, discard) is purely additive and MUST be supported without violating append-only, provided such branches are not pushed to live stream refs.
14.2. Counterfactual past (rewrite history to ask “what if event K differed”) MUST occur only on clearly-marked throwaway refs that never become canonical. This is the one place history rewrite is safe.
14.3. Implementations MUST NOT push what-if branches to live stream refs or the authoritative remote. Deleting a local throwaway branch (git branch -D) leaves objects reachable until GC after the configured grace period, giving a recovery window; implementations relying on this MUST NOT assume immediate reclamation.
14.4. Integration tests SHOULD use real historical stream states as fixtures (check out the store at any commit; replay deterministically via the pure decider) rather than synthetic ones.
14.5. Deferred (open design axis). The concrete marking/enforcement mechanism for throwaway refs — a dedicated ref namespace, a GES-WhatIf trailer, and/or hook-level rejection at the authoritative remote — is deliberately deferred until what-if branching is implemented; until then, 14.1–14.3 are review-level obligations. The enforcement subtlety to solve then: a speculative-future branch (14.1) fast-forwards from the tip, so an accidental push is indistinguishable from a legitimate append to a plain fast-forward-only hook.
15. Security and append-only enforcement
Section titled “15. Security and append-only enforcement”15.1. The authoritative remote MUST enforce append-only on refs/ges/<store>/global, refs/ges/<store>/streams/* and refs/ges/<store>/snapshots/* via a server-side pre-receive hook (or platform ref-protection) that rejects any non-fast-forward update and any deletion of those refs, except the specific horizon-truncation operation of §13.2 performed by an authorized process.
15.2. Stream tips SHOULD be signed (§7.4). Verifiers SHOULD validate signatures and compare tips against a known-good anchor.
15.3. Transcript/payload confidentiality: any party with repository read access can read every event on a stream ref. Implementations MUST document this and SHOULD offer per-stream encryption (§13.3) where events may carry sensitive data.
16. Conformance requirements (MUST-list)
Section titled “16. Conformance requirements (MUST-list)”A conforming implementation:
- C1. Places every GES ref under a store segment per §3.2 — the global stream at
refs/ges/<store>/global, state streams understreams/, tag indices undertags/, snapshots undersnapshots/— and uses no otherrefs/ges/*purpose beyond those and the refs a claimed extension defines under the same segment. (§3, §3.1.1) - C2. Encodes stream names reversibly into valid ref components and stores the raw name in each record. (§4)
- C3. Stores one whole event per file, never a diff; one commit per event; a multi-event append is a commit chain made visible by one atomic ref CAS; derives order from ancestry + in-record position, not directory listing. (§5)
- C4. Serializes records with the §6.2 schema and computes all hashes over RFC 8785 JCS. (§6)
- C5. Writes the §7.3 trailers so stream version is O(1)-readable from the tip commit. (§7)
- C6. Derives event ids per §8.2, deduplicates by id, and (if chaining enabled) maintains a per-stream SHA-256
predecessorHashchain. (§8) - C7. Performs every append as an atomic compare-and-swap against the tips observed at read time, across every affected ref; never produces a lost update; evaluates any append condition and commits the append as one indivisible step. (§9)
- C8. Implements
appendToStream,readStream,aggregateStreamwith the §10 behaviors, in-memory, without working-tree checkout. - C9. Produces byte-identical
aggregateStreamresults across runs and implementations for identical inputs. (§10.3) - C10. States its durability level (L0/L1/L2); at L1/L2 configures
core.fsyncto include at leastloose-object,reference, orders object durability before ref durability, and never claims durability while using a page-cache-only write path; documents any L1 loss window. (§11) - C11. Distinguishes local/shared/incoming via remote-tracking refs and treats them as point-in-time copies. (§12)
- C12. Never auto-merges divergent single-stream histories; rejects the losing concurrent push. (§12.5)
- C13. Distinguishes snapshot from truncation and only truncates behind a replicated horizon with re-anchoring. (§13)
- C14. Never pushes what-if branches to live refs. (§14)
- C15. The authoritative remote rejects non-fast-forward and deletion on GES refs. (§15)
- C16. Reads a stream’s tip by direct ref resolution, never by namespace listing; uses packed-refs or reftable for many-stream stores. (§3.6)
- C17. Never depends on large flat-directory enumeration for correctness or ordering. (§5.3, §5.4)
- C18. (If shadow capture is offered) Captures working-tree state via a temporary index and plumbing without touching the user’s
HEAD, index, or working tree; supports scoped capture of a supplied path set (including deletions/renames); keeps shadow refs local and never pushes them. (§3.4.1, §3.4.1.1, §3.3.1) - C19. (If shadow capture is offered) Encodes the base
HEADand a stable worktree discriminator in the shadow ref name, runs capture in the agent worktree’sGIT_DIRcontext, re-anchors onHEADmovement, relies on the ref CAS to serialize concurrent condensation across worktrees, and links code commits to appends via a resolvableGES-Linktrailer. (§3.4.2, §3.4.2.3, §3.4.3, §3.4.6) - C20. Stores events in a human-/agent-readable text encoding (JSON); does not make a binary/schema encoding the default; treats analytical/columnar storage only as a derived read model. (§6.3)
- C22. Maintains the global stream per §20 (dual view, sequencer designation, fast-forward-only reconciliation) and satisfies the CB clauses of §20.11; tests CB-a/b/c are core-required.
- C23. Exposes the DCB core interface per §10.0 and §18c:
read(query)andappend(events, condition), with the condition evaluated exactly against the global stream and committed in the atomic multi-ref transaction of §20.4.1; maintains tags and a tag index per §18b. (§10.0, §18b, §18c) - C21. (If export is offered) Exports a stream as ordered, self-contained JSONL via plumbing without any checkout, streamed not buffered. (§16d)
16.1 Conformance test suite (acceptance criteria)
Section titled “16.1 Conformance test suite (acceptance criteria)”Each test below MUST pass. These double as the goal-driven success criteria for the implementation brief. They test the framework-neutral DCB core (conditional append, query read, integrity, durability, distribution); they are independent of any framework binding. Some tests use the Emmett method names (appendToStream, readStream) purely as the reference binding — a non-Emmett store satisfies the same test by invoking the equivalent core operation. Emmett-interface compatibility is a separate, optional profile (§10), not a core conformance requirement.
- Round-trip. Append 3 events,
readStreamreturns them in order with all §6.2 fields intact. - Atomic multi-event append. Append 5 events in one call → exactly 5 one-event commits chained on the prior tip, made reachable by a single ref move; version advances by 5. Simulate a crash after building the commits but before the ref update → none of the 5 are visible and the stream is unchanged.
- Optimistic concurrency. Two appends against the same observed tip: the first succeeds, the second fails with a concurrency error; no event is lost; the stream has exactly the first append’s events.
- Empty boundary. Appending under a condition whose query selects the target state id with
after = 0succeeds only while no matching event exists; a second such append fails. (Emmett shorthand:STREAM_DOES_NOT_EXIST.) - Exact boundary. Appending under a condition naming sequence position
psucceeds iff no event matching its query exists afterp. (Emmett shorthand:expectedStreamVersion = k, mapped perextensions/emmett-binding.md§10.1.1.) - Idempotency. Re-appending an event with an already-present derived id at the same position is a no-op.
- Determinism.
aggregateStreamover the same events yields byte-identical state across two independent runs and (if a second implementation exists) across implementations. - No checkout. After any number of appends,
HEADand the working tree are unchanged; no branch was checked out. - Chain. Tamper with one event’s
dataon a copy; chain verification detects the break at that position. - Snapshot equivalence.
aggregateStreamstarting from a snapshot equals full replay from position 1. - Union merge. Two clones append to different streams, then fetch/merge: both streams present, no conflict.
- Same-stream divergence. Two clones append to the same stream from the same tip; on push the second is rejected (non-fast-forward), not silently merged.
- Append-only enforcement. A non-fast-forward push to a stream ref against a hook-enabled remote is rejected.
- Agent-readability smoke test.
git log --format=%s refs/ges/<store>/streams/<enc>yields the human summaries;git show <tip>shows the appended event files as readable JSON. - Durability at L2. At level L2, append an event, then simulate an unclean shutdown (drop page cache / kill without flush per the platform crash-consistency harness); after recovery the stream tip and its objects are present. At L0 the store is labeled non-durable and the test is skipped, not silently passed.
- L1 loss window (if offered). At level L1 with window
T/N, appends within the current window MAY be lost on crash but no append older than the last flush is lost, and the store never reports a lost append as durably acknowledged. - Agent 1:1 mapping.
git log --oneline refs/ges/<store>/streams/<enc>shows one line per event;git show <sha>shows exactly one event as readable JSON. - Direct tip resolution. Reading one stream’s version/tip performs a direct ref lookup and does not enumerate
refs/ges/<store>/streams/; verified by call tracing or by scaling to many streams and asserting O(1) tip-read time. - Shadow capture is side-effect-free (if offered). With dirty working-tree changes, trigger a capture; assert
HEAD,.git/index, and the working tree are byte-for-byte unchanged, a newrefs/ges/<store>/shadow/<HEAD7>-<wt>commit exists, andgit diff HEAD <shadow-tip>equals the uncommitted delta. - Bidirectional link (if offered). After condensation, the code commit resolves to its append via
GES-Link, andgit log --all --grepfrom the append id resolves back to the code commit; a push carries the condensate but norefs/ges/<store>/shadow/*. - Scoped capture (if offered). Given a dirty working tree and a path set covering only some of the changes, the capture tree equals
HEADoverlaid with exactly those paths (including a deletion and a rename in the set), and unreported changes are absent. - Separate-worktree agents (if offered). Two
git worktreecheckouts on different branches each capture and condense concurrently; captures anchor to their own worktreeHEAD, shadow refs do not collide (distinct<worktree-id>), and concurrent condensation into a shared stream is serialized by the ref CAS with no lost update. - JSONL export (if offered). Export a stream: output is one self-contained JSON record per line in event order,
HEAD/index/working tree are unchanged (no checkout), and re-importing the JSONL into a fresh stream reproduces byte-identical records. A ranged export returns exactly the requested position span. - Ref-encoding round-trip.
decode(encode(x)) == xfor a fuzz set including/, spaces, unicode, and.lock; the raw stream name is present in each record; invalid characters are percent-encoded and no component is empty, ends in.lock, or contains... (E-4-1/2/3) - Canonicalization & id vector. A fixed event serializes to the published RFC 8785 JCS bytes and yields the published
sha256:id; changing any field changes the id. (E-6-2, E-8-2, E-NEW-vectors) - Non-existent stream read.
readStreamon an absent stream returns an empty result, not an error. (E-10-3) - Zero-event append.
appendToStreamwith no events creates no commit and moves no ref. (E-10-4) - Trailer version read. The tip commit carries
GES-Version/GES-Stream/GES-Position; reading the stream version touches only the tip commit object and does not read the tree. (E-7-1, E-7-2) - fsync configuration (L1/L2). At L1 or L2 the store’s repository has
core.fsyncincluding at leastloose-object,reference. (E-11-2) - Threshold repack. When the loose-object count exceeds the configured threshold, a repack runs and the loose-object count drops. (E-13-6)
- DCB conditional append. A conditional append evaluates its query against the global stream after the given sequence position and commits in one atomic
git update-ref --stdintransaction; a concurrent matching event aborts the whole append with no ref moved; a conjunctive query (tag A and tag B and type T) conflicts only with an event matching all three, not with one matching a single term. (E-18c-1, E-18c-2a, E-18c-2b, E-18c-5)
16.2 Review checks (non-runtime obligations)
Section titled “16.2 Review checks (non-runtime obligations)”Verified by inspection or configuration review, not by the runtime suite. Each maps to an EARS requirement in the companion catalog.
- RC-1 The durability level (L0/L1/L2) is stated. (E-11-1, C10)
- RC-2 The substrate topology, the designated global-stream sequencer (E-20-1), and the claimed extensions are declared. (E-12-2, C11, C22)
- RC-3 Documentation obligations are present: working-tree-materialization consequences (E-6-7); fsync-bound throughput (E-11-5); non-deletability of distributed plaintext (E-13-4b); unbounded growth without retention (E-13-5); universal read access on readable refs (E-15-3); native scan not a hot path (E-16b-3); the window before an offline append under a boundary is globally validated (E-18c-4).
- RC-4 Design invariants hold under code review: SHA-guard vs. stream version-contract never conflated (DI-1/E-9-1); disk cost reasoned from packed size (DI-2/E-6-8).
- RC-5 The storage-core seam is present and all Git/object/ref/fs operations route through it (E-10-6); Git subprocesses use
execFile/spawnnot a shell (E-10-7); the hot path avoids per-event forks (E-10-8); non-ref-CAS critical sections use a maintenance lock (E-10-9).
Full requirement→verifier traceability is in the companion ges-review-and-ears.md, Part 3.
18. Prior art and references
Section titled “18. Prior art and references”- Emmett — GES’s first and reference framework binding (§10), and a consumer of GES, not its definition: reference
EventStoreinterface (readStream/appendToStream/aggregateStream), event shape{ type, data, metadata },BigIntStreamPosition, optionalglobalPosition. Other frameworks bind the same core (§10). https://event-driven-io.github.io/emmett/ - EventSourcingDB (the native web / Golo Roden) — purpose-built event store with append-only storage, global ordering, atomic multi-stream writes, optimistic concurrency via preconditions (including query-based, i.e. DCB), a predecessor-hash chain + audit endpoint + optional signatures, CloudEvents format, NDJSON streaming, and EventQL for ad-hoc cross-subject queries. The closest independent convergence with GES’s integrity and atomicity design; the main reference for the §21 alignment register. https://docs.eventsourcingdb.io/
- EventStoreDB / KurrentDB — the reference stream database: streams + the global
$alllog, per-streamStreamPositionand global commit/preparePosition, catch-up and persistent subscriptions. https://developers.eventstore.com/ - Marten — Postgres event store + document DB; single-table event store with global sequence, inline/async projections, a DCB implementation. https://martendb.io/
- Message DB — a minimalist Postgres event/message store (streams, categories, global position). https://github.com/message-db/message-db
- Entire — independent validation of “session history is code”: agent metadata on an orphan branch (
entire/checkpoints/v1), conflict-free by random ids + tree union, linked to commits via a trailer. https://github.com/entireio/cli · https://julien.danjou.info/blog/how-entire-works-under-the-hood - Irmin — a database on Git’s design principles with programmable merge functions and Git on-disk compatibility; the reference for “Git-as-data-store with custom merges.” https://github.com/mirage/irmin · https://irmin.org/
- Dolt — Git-for-data with cell-level version control; the answer to “SQLite-in-Git can’t merge.” https://github.com/dolthub/dolt
- DCB — Dynamic Consistency Boundaries (Sara Pellegrini, “Killing the Aggregate”): tags, queries, conditional append. https://dcb.events/ · reference implementations incl. Marten (https://martendb.io/events/dcb.html) and the
eventsourcinglibrary. - Conventional Commits + semantic-release — a production-scale, latent instance of the GES thesis: typed events on an append-only git log, folded by a deterministic projection into version + changelog, with the release tag as a snapshot and the last tag as the cursor. Validates “git log as event log” and the projection-over-log / tag-as-snapshot pattern (§16c), while illustrating its limits (single stream, weakly-typed events, read-side only). https://www.conventionalcommits.org/ · https://github.com/semantic-release/semantic-release
- RFC 8785 — JSON Canonicalization Scheme (JCS).
- RFC 2119 / RFC 8174 — requirement keywords.
18b. Tags
Section titled “18b. Tags”Tags are the core selector of the DCB model (§10.0): a query selects events by type and by tags. A GES tag is not a Git tag (a named ref), and it has no Emmett equivalent — in Emmett the same information is a plain data/metadata field (§10.0).
18b.1. An event MAY carry tags: string[] of key:value pairs in its record (§6.2). The array MAY be empty, in which case the event is selectable by type alone. Tags are authoritative in the record and SHOULD be denormalized as GES-Tag: commit trailers.
18b.2. There are no portable custom commit-object headers; commit headers are fixed. Tag-like attributes therefore live in the record (authoritative) and in trailers (denormalized index). git notes is unsuitable because it is mutable and would break tag immutability.
18b.3. A store MUST maintain a tag index per tag, as one ref under refs/ges/<store>/tags/ (§3.2), rebuildable from the records, so that a query need not walk the whole global stream. An event with N tags is referenced from N index entries; because blobs are content-addressed, the underlying event object is shared, so indexing costs references, not copies. Where the index evaluates an append condition, §9.2.2 governs.
18b.4. Every event MUST carry a tag identifying its state stream, so that the derived stream view (§10.0) is expressible as a query and the two agree by construction. The encoding of that tag is implementation-defined and MUST be documented.
18c. Dynamic Consistency Boundaries
Section titled “18c. Dynamic Consistency Boundaries”18c.1. The model. The core uses a single event sequence in which each event has a type, opaque data, and any number of tags, and is assigned a sequence position. A query is a set of query items combined with OR. Each item MUST carry at least one non-empty constraint — a set of types, a set of tags, or both — and an event matches an item when its type is one of the item’s types and it carries every tag of the item. A query whose item set is empty selects every event; an item carrying no constraint is ill-formed, not a wildcard. An append condition (failIfEventsMatch + optional after) fails the append if any event matching the query exists after the given position. This is the interface of §10.0 and the write precondition of §9.1.1.
18c.2. Position in GES. DCB assumes a globally ordered log with a store-wide position; the global stream (§20, C22) supplies it, authoritative once sequenced. No separate per-tag anchor mechanism is required: earlier drafts of this specification approximated the condition with per-tag anchor refs because the global stream was optional, and that approximation is withdrawn (§20.9).
18c.3. Enabling primitive. The conditional append MUST use Git’s atomic multi-ref transaction (git update-ref --stdin, all-or-nothing, with per-ref old-value preconditions), as required of every append by §20.4.1: in one transaction, verify each affected ref at its read-time tip and advance the global stream, the state streams, and the tag indices of the appended events. If any precondition fails, the whole append aborts as a concurrency conflict.
18c.4. Precision. Because one authoritative remote serializes (§20.5), the check against the global stream is exact: conjunctive and typed queries (an event matching tag A and tag B and type T) are enforced precisely, without the false conflicts a per-tag approximation would produce. The cost is a scan rather than a ref compare (§9.2.4).
18c.5. Distribution crux. A consistency boundary is a global invariant. Two clones appending offline under the same boundary conflict, detectable only at push (non-fast-forward, §12.5) and not auto-mergeable. Enforcement is therefore locally optimistic and globally authoritative at the sequencer (§12.4, §20.5); since every conforming store designates one (C22), the enforcement authority always exists, and it inherits N1. Offline appends under a boundary are validated when sequenced. Implementations MUST document this window.
18c.6. Alignment with the DCB specification (https://dcb.events/specification/). The DCB interface is read(query, options) and append(events, condition); in GES this is the core interface (§10.0), and the stream-based appendToStream / readStream that Emmett binds is the derived view over it. GES stores on Git rather than in a purpose-built store (the DCB spec explicitly permits different term and function names). DCB assigns each event a Sequence Position that is unique and monotonic increasing and MAY contain gaps; in GES that position is the event’s commit depth in the global stream — one event is one commit (§5.2), so GES positions events, not writes, exactly as DCB requires, with no intra-write sub-ordering needed. The authoritative remote supplies the store-wide monotonic position (§20); rebase-driven reassignment (§20.7.1) is the mechanism by which positions may in practice contain gaps or shift before a push is authoritative. Where GES aligns on scope: the DCB model puts one event stream in one bounded context, and GES realizes that as one store holding one global stream (§20.1.1). Where GES deviates: it additionally maintains a mandatory per-state-id stream view (§10.0), which pure DCB does not have.
19. Spec evolution
Section titled “19. Spec evolution”19.1. gesVersion in records and the GES-Version trailer track the format version. Breaking changes MUST increment it and define a migration.
19.2. This is a draft. Open questions for public review: whether to standardize an OPTIONAL synthetic global order; a canonical snapshot record schema across implementations; a portable encryption envelope for §13.3; whether to adopt a CloudEvents-compatible record envelope (§21) for interoperability; and whether to define a richer ad-hoc query layer (EventQL-style, §21) beyond native scan and projections. The last two are marked in the §21 register as open and should be resolved against a running implementation, not on paper.
20. The global stream — dual-view over one authoritative remote
Section titled “20. The global stream — dual-view over one authoritative remote”Sections 3–15 describe the mechanics of the multi-master substrate: distributed, per-stream, offline-multi-writer. State streams alone provide no global total order; this section defines the global stream — the mandatory primary ordering every conforming store maintains (§1, C22) — and the conventions that bind it to one authoritative remote. Multiple appenders are permitted; what is “single” here is the single authoritative remote (a convention, §20.5), not a single writing process; a store with no remote is trivially its own sequencer. The global stream keeps every read-side property (readable, signable, replayable records; every clone a full verifiable backup). The record format (§6), identity/integrity (§8), durability (§11), compaction (§13), and export (§16d) apply to it unchanged; this section defines the additional layout, ordering, concurrency, and distribution rules the global stream adds on top of the substrate mechanics.
20.1 Operational envelope
Section titled “20.1 Operational envelope”The global stream is mandatory (C22); this subsection states the envelope its design is optimized for, not a choice to make. The design assumes deployments that agree on one authoritative remote, keep sovereignty over reads, accept the sequencing window on offline writes (§20.3.2), and run at low-to-moderate append frequency; it serves use cases that reason over one ordered stream or need precise dynamic consistency boundaries (§18c). Multiple appenders are fine; reads remain fully distributed. Only the authority over global order is centralized (in a remote), not the act of writing.
20.1.1. One store is one bounded context. The DCB model puts a single event stream in one bounded context, and it requires a sequence position to be unique within the event store. In GES that stream is the global stream, so “global” means store-wide, never universe-wide: the total order covers one bounded context’s events. GES defines no ordering relation across stores; an event crossing from one context to another is a separate published fact in the receiving context, carrying that context’s own sequence position, never a shared one. This also bounds the evaluation cost of §9.2.4 — an append condition scans one context’s stream, not an organization’s.
20.1.2. A repository may host several stores. The ref layout carries a mandatory store segment (§3.1.1, §3.2), so one repository can hold two bounded contexts side by side, each with its own global stream and its own total order, and neither ordering the other. This is what keeps the linkage extension (§3.4) usable on a monorepo: events stay co-located with the code commits that justify them even when that repository serves more than one context. The segment is also what gives the global stream and each tag index a defined ref name, which an earlier revision left unnamed.
20.2 Dual-view structure
Section titled “20.2 Dual-view structure”Events are stored once as content-addressed blobs and are referenced by more than one commit chain (Git’s structural sharing; a blob referenced by many trees exists once). The store maintains:
- The global stream (a.k.a. the global log) — the primary commit chain at
refs/ges/<store>/global(§3.2), one commit per event in append order (§5.2). An event’s global stream position is its depth in this chain. The global stream is the reachability root: an event blob survives as long as the global stream references it, so secondary views may be pruned and rebuilt without losing events. - State streams — one secondary stream per state id under
refs/ges/<store>/streams/<encoded>(the substrate layout, §3), containing exactly that stream’s events in stream order. This is the state-optimized view:aggregateStreamwalks the short state stream rather than filtering the global stream. - Tags — maintained as lean indices under
refs/ges/<store>/tags/(§3.2): a ref pointing at a position/id list, not a full commit chain, unless a tag view is explicitly required as a walkable history (§20.8).
All chains and indices MUST reference the same event blob for a given event; an implementation MUST NOT duplicate event payloads across views.
Figure (informative): Dual-view structure — events stored once, referenced by multiple streams. The global stream (primary) chains all events by append order; state streams (secondary) each chain a single stream’s events by stream order. Each event’s blob is stored once and structurally shared: the global-stream commit references it, and the corresponding state-stream commit references the same blob. Tag indices may be lean refs (id lists) rather than full chains.
gitGraph LR: commit id: "ev1" tag: "global-pos=1" commit id: "ev2" tag: "global-pos=2" commit id: "ev3" tag: "global-pos=3" commit id: "ev4" tag: "global-pos=4"Caption: Diagram shows the global stream’s total order. Independently, the state stream for subject order-123 references events 1 and 3 (ev1, ev3) in its stream order, while state stream payment-456 references events 2 and 4 (ev2, ev4). Each event blob exists once in the object store; multiple stream refs (global + per-state) point to different commit chains that tree-reference the same blobs.
20.3 Appenders and global stream position
Section titled “20.3 Appenders and global stream position”20.3.1. Under the global stream, appends to a given repository are serialized locally by the appending process, so the §9.2 compare-and-swap collapses to an in-process check of the in-memory tips. Multiple appenders across repositories are permitted; they are reconciled at the authoritative remote (§20.5, §20.6), not by a “single writer” rule.
20.3.2. An appender MUST assign each event’s global stream position as its depth in the global-stream chain. The global stream position of a locally-appended event is provisional until it is accepted by the authoritative remote (§20.5); consumers MUST NOT treat a global stream position as authoritative until it is pushed.
Figure (informative): Provisional and sequenced global stream positions. Clone A appends events with provisional global stream positions; Clone B pushes first, advancing the authoritative remote’s global stream. Clone A’s push is rejected; Clone A fetches, rebases its events onto the new global-stream tip (which reassigns both global stream positions and SHAs), and re-pushes. Only then are the events sequenced/authoritative. The gitGraph shows Clone A’s initial state (diverging from the remote); the flowchart shows the lifecycle.
gitGraph LR: commit id: "remote-e1" tag: "authoritative-tip" branch clone-a commit id: "local-e2-prov" commit id: "local-e3-prov" tag: "diverges-here" checkout main commit id: "remote-e2" tag: "after-clone-b-push"flowchart LR A["Clone A: local append<br/>e2, e3 @ positions 2, 3<br/>(provisional)"] -->|Push attempt| B["Authoritative remote<br/>already advanced<br/>(Clone B won the race)"] B -->|Rejected non-FF| C["Clone A: fetch +<br/>rebase onto new tip<br/>(new SHAs, positions 3-4)"] C -->|FF push| D["Clone A: sequenced<br/>e2, e3 now authoritative<br/>@ positions 3, 4"]Caption: Clone A’s events initially have provisional global stream positions (2, 3). When the authoritative remote advances (Clone B’s push succeeded), Clone A’s push is rejected (non-fast-forward). Clone A rebases, reassigning both global stream positions (now 3, 4) and creating new commit SHAs; re-pushing then succeeds, making the events sequenced and authoritative.
20.4 Atomic multi-view append
Section titled “20.4 Atomic multi-view append”20.4.1. Appending one event joins it to the global stream and its state stream and updates any tag index. The implementation MUST build all objects in memory (§10.1) and then make every affected ref visible in a single atomic transaction (git update-ref --stdin, all-or-nothing).
20.4.2. If the transaction does not complete, then no view MUST reflect the event: a crash between object creation and the ref transaction leaves only unreferenced objects and every view unchanged. Implementations MUST NOT advance one view’s ref without the others; partial visibility across views is non-conforming (it would let the global stream and a state stream disagree about whether an event exists).
20.5 The authoritative remote (default name origin)
Section titled “20.5 The authoritative remote (default name origin)”20.5.1. Multiple checkouts find a shared truth by convention: they agree on one remote — the authoritative remote — as the source of truth. Its default name is origin. It MUST enforce fast-forward-only and append-only on the GES refs (receive.denyNonFastForwards plus the §15 pre-receive hook), except the authorized horizon-truncation of §13.2. Its global-stream chain is the single source of truth for global order. Authority is a role assigned to a remote, not a property of a node: there is no election and no privileged server — only the agreement about which remote is canonical.
20.5.2. An append is authoritative only once its push to the authoritative remote succeeds as a fast-forward. Until then it is local and provisional (§20.3.2). This is the position-level form of the visible-vs-durable distinction (§11).
20.5.3. Topologies (the role is rebindable). Because authority is a convention over a remote, the same fast-forward-only + pull --rebase mechanics (§20.6) work against any remote, so the global stream’s single authoritative remote is one topology, not the only one:
- Single shared remote (this section): one convention, one global truth — the simplest and recommended default.
- Hierarchy of remotes: appenders push to team remotes that push to an umbrella remote (the Linux maintainer-tree model); each level is a local truth flowing upward.
- Federation / peer-to-peer: several remotes, each repository following a subset; truth is per federation agreement.
- Single-node: one repository is the truth, no remote.
Each additional authoritative remote reintroduces the question “which global order is canonical”: two umbrella remotes can diverge, returning to the multi-master reconciliation problem at the remote level. The single shared remote is simplest precisely because it makes the global order unique. GES specifies only the single-remote topology in this version; the others use the same primitives and are deferred (§20.10), and each added authority MUST be understood to trade away global-order uniqueness.
20.6 Reconciliation: pull --rebase + fast-forward-only
Section titled “20.6 Reconciliation: pull --rebase + fast-forward-only”20.6.1. If a push is rejected as non-fast-forward (the authoritative remote advanced since the appender last fetched — e.g. a second appender), the appender MUST reconcile by: fetch the authoritative remote’s global-stream tip; rebase its own not-yet-pushed events onto that tip; re-push; repeat until the push fast-forwards.
20.6.2. This rebase is conflict-free by construction: events are absolute facts, not diffs (§5.1), and new events touch only the tip, so re-parenting them onto a new base never produces a textual conflict. The global stream therefore relies on Git’s native pull --rebase + fast-forward-push idiom rather than any bespoke merge; where the global stream is tracked as a branch, git pull --rebase is literally the mechanism, and the plumbing equivalent (re-parent the unpushed commits) applies to the refs/ges/ namespace.
20.6.3. Stream-level concurrency is the one genuine conflict: if two appenders wrote to the same stream, the state stream’s expected version (§9.2.3) MUST reject the loser, which then re-reads and re-decides (§9.5). With a single appender this does not arise; the rebase path handles only global-position reassignment across independent streams.
20.7 Consequences of rebase
Section titled “20.7 Consequences of rebase”20.7.1. Positions are reassigned. Rebased events receive new global stream positions (after the authoritative remote’s intervening events). Stream positions are unchanged unless the same stream was concurrently appended (§20.6.3).
20.7.2. SHAs change, so signatures must follow. Rebasing creates new commits (new parent → new SHA). Implementations MUST NOT rely on per-event commit signatures in the global stream; they MUST anchor integrity at the tip, at snapshots, or at tags (§13.5.2) and (re-)sign only the new tip after a rebase.
20.7.3. All views of a rebased event move together. Because the global-stream commit for an event is rebased to a new SHA, every secondary view referencing that event MUST be updated in the same atomic transaction (§20.4), or a view would point at an orphaned pre-rebase commit. View consistency under rebase is the same invariant as view consistency under append.
20.8 Full chain vs lean index, per view
Section titled “20.8 Full chain vs lean index, per view”20.8.1. Each view type is either a full commit chain (walkable, greppable, signable history) or a lean index (a ref to a list of positions/ids). The global stream and state streams SHOULD be full chains (they are read and folded directly). Tag views SHOULD be lean indices, because per-event commit+tree overhead (§13.5.1) multiplies by the number of chains an event joins; materializing every tag as a full chain pays that overhead × tag-count for little benefit.
20.9 What the global stream adds to the substrate mechanics
Section titled “20.9 What the global stream adds to the substrate mechanics”- Global order. A global total order exists (the global stream);
globalPositionis defined and authoritative once pushed. - §12.5 (union-merge) — scoped to state streams. State streams keep conflict-free union semantics (§12.5, §1). The global stream itself never unions: divergence against the designated sequencer is prevented by fast-forward-only reconciliation (§20.6), not reconciled after the fact.
- §9 concurrency — simplified. Local append is an in-memory check (§20.3.1); cross-appender safety is the authoritative remote’s fast-forward-only push.
- §9.5 (conflict policy) — reduces to the
pull --rebaseretry loop (§20.6), plus stream-level expected-version rejection for same-stream concurrency. - §18c (DCB) — is precise. The consistency check runs against the authoritative remote’s consistent global stream, so conjunctive and typed queries are enforced exactly (§18c.4). This is what allows DCB to be the core model rather than an approximation layered on top: no per-tag anchor mechanism is needed where one authoritative remote serializes (§18c.2).
- N1 (replicated durability) — unchanged as a non-goal, but the authoritative remote is now the natural durability anchor.
20.10 Out of scope for the global stream (future)
Section titled “20.10 Out of scope for the global stream (future)”Multiple authoritative remotes, and leader election / write-side failover for a remote, are out of scope for this version. A deployment whose sole authoritative remote is unavailable has writes unavailable until it is restored; production hardening (a highly-available remote with failover) reintroduces consensus and is deferred. The multi-remote topologies of §20.5.3 use the same primitives and are explicitly left for a later revision.
20.11 Global-stream conformance clauses (core)
Section titled “20.11 Global-stream conformance clauses (core)”In addition to the shared requirements, a the global stream implementation:
- CB-1 Designates one authoritative remote as the global-stream sequencer (default
origin) enforcing fast-forward-only + append-only on GES refs. (§20.5) - CB-2 Makes every view of an event (global stream, state stream, tag index) visible in one atomic ref transaction; never exposes partial cross-view visibility, on append or on rebase. (§20.4, §20.7.3)
- CB-3 Treats a local global stream position as provisional until pushed; exposes it as authoritative only after a fast-forward push. (§20.3.2, §20.5.2)
- CB-4 Reconciles a rejected push by
pull --rebase+ re-push, conflict-free, and re-signs only the new tip. (§20.6, §20.7.2) - CB-5 Stores each event payload once and references it from all views; never duplicates payloads across chains. (§20.2)
Test CB-a. Two appenders append independently and both push; the second is rejected non-fast-forward, rebases its events onto the authoritative remote’s tip conflict-free, and re-pushes successfully; afterwards the global stream contains all events in a single total order and every event’s state stream and the global stream agree on its existence.
Test CB-b. Kill the appender between building an event’s objects and the ref transaction; after restart, no view references the event and all views agree it does not exist.
Test CB-c. Append an event that belongs to one stream and two tags; assert exactly one blob for the payload, referenced from the global stream, the state stream, and (if full-chain tags are configured) the tag chains.
21. Alignment with established event stores (deviation register)
Section titled “21. Alignment with established event stores (deviation register)”Event sourcing has been solved repeatedly; this register maps the cross-store consensus to GES’s stance and states every deviation with its reason (§1). Reference stores: EventStoreDB/KurrentDB, EventSourcingDB (the native web), Marten, Message DB, Emmett. New GES revisions MUST update this register when a decision changes its relationship to the consensus.
| Established practice (consensus) | GES stance | Reason / note |
|---|---|---|
| Append-only, strictly immutable events | Aligned (§5, §8) | Immutability is convention in Git, enforced via protected refs (§15) — a substrate deviation in mechanism, not in intent. |
| Events grouped into streams by a key / subject | Aligned (§3) | EventSourcingDB uses hierarchical /-paths (subjects) with recursive reads; GES uses flat encoded stream names. Hierarchical subjects are an open option (§19), not mandated. |
| Atomic multi-event (and multi-stream) write | Aligned (§5.2.1 append chain; §20.4 atomic multi-view) | Direct convergence with EventSourcingDB’s atomic multi-stream writes. |
| Optimistic concurrency via expected version | Aligned (§9) | EventSourcingDB generalises this to preconditions, including a query-based precondition — i.e. DCB as a first-class write condition. GES’s §18c is the same idea; worth mirroring the precondition framing. |
A global total order (global stream / $all / global stream position) |
Aligned (§20, C22) | The mandatory global stream provides the global order via a single sequencer. The substrate alone (offline appends before sequencing) has only per-stream order — the CAP trade is confined to the sequencing window, during which positions are provisional (§20.3). |
| Integrity: predecessor-hash chain, optional signatures, audit verification | Aligned (§8.3 chain, §7.4/§15 signing, §8.6 verify) | Strong convergence with EventSourcingDB (predecessor-hash chain + audit endpoint + optional digital signatures). GES gets the Merkle chain from Git natively (§8.1). |
| Snapshots as first-class | Aligned (§13) | — |
| Catch-up subscriptions / “observe” for read models and the transactional-outbox pattern | Aligned (§16c) | EventSourcingDB’s observe = replay + live subscription = outbox without extra infra; GES’s projection + hook (§16c.4) is the same shape, minus a built-in push. |
| Idempotency / dedup | Aligned (§8.2.2) | — |
| Read models via projections; ad-hoc exploration via a query layer | Partially aligned (§16b/§16c) | GES has projections but no query language; it offers native scan (pickaxe/grep) for ad-hoc. EventSourcingDB’s EventQL (cross-subject queries, NDJSON results, for DCB) is a richer ad-hoc layer GES lacks — an open option (§19). |
| Standard, interoperable event format (CloudEvents) | Deviates (§6.2 custom record) | Open decision (§19): EventSourcingDB standardises on CloudEvents (structured JSON, specversion/id/subject/type/data, traceparent/tracestate for OpenTelemetry). GES currently defines its own record; adopting a CloudEvents-compatible envelope would improve interoperability at the cost of extra fields. Not yet justified either way. |
| NDJSON / JSONL as the streaming interchange | Aligned (§16d) | EventSourcingDB streams events and query results as NDJSON; GES’s JSONL export is the same format. |
| Purpose-built DB, single binary, HTTP API, no external deps | Deviates (Git substrate, §6.3.5) | Justified by GES’s goals: sovereignty, code+events co-location, human-/agent-readable and signable records, every clone a verifiable backup, no server to operate. This is the one foundational deviation from which most others follow. |
| Naming of the ordering/authority role | Named for what it is (§2 “authoritative remote”) | Real event stores call the write-coordinating node leader (EventStoreDB, Axon Server) — a consensus-elected node with failover, machinery GES lacks; the shared-log literature (Corfu) says sequencer — a dedicated ordering component GES also lacks. GES’s authority is a convention over a remote, so it is named authoritative remote (default origin); writer/appender (from Axon’s EventAppender; “write” from Message DB / EventSourcingDB) and decider (functional ES) name the other roles. |
Scope note (evidence discipline): the alignment above compares GES only against real event stores (EventStoreDB, EventSourcingDB, Marten, Message DB, Emmett; plus the shared-log literature as contrast). Streaming systems (Kafka, Pulsar) are a different category — event streaming is not event sourcing (partition-local ordering, no per-stream expected-version, no store-wide consistency boundary) — and are used here only as contrast, never as evidence for a GES decision.
Reading the register: most rows are Aligned — GES is, deliberately, a fairly conventional event store wearing a Git substrate. The deviations are few and each traces to either the substrate (Git) or a stated goal (distribution, sovereignty). The two genuinely open items — a CloudEvents-compatible record and richer ad-hoc query (EventQL-style) — are decisions to make against a running implementation, not to settle on paper (§19).
Git Event Store is an independent open specification for an event store that uses Git as its storage component. It is not affiliated with or endorsed by the Git Project or Software Freedom Conservancy. Git and the Git logo are either registered trademarks or trademarks of Software Freedom Conservancy, Inc., corporate home of the Git Project, in the United States and/or other countries.
Git Event Store is not affiliated with or endorsed by Kurrent, the maker of KurrentDB (formerly EventStoreDB). "Event store" is used here as the generic name of the event-sourcing storage pattern.