Reference
EventStore
Reference for go-sequent's EventStore: the Store interface, memory and Postgres backends, errors, per-aggregate SHA-256 hash chain with canonical input, and verification.
EventStore
Package: eventstore
The event store persists commands and aggregate streams. It enforces optimistic concurrency and unique keys on append, and it maintains a hash chain over stored events. Applications usually reach it through the Repository, not directly.
Interfaces
type Store interface {
Append(ctx context.Context, cmd CommandRecord, streams []StreamAppend) error
LoadStream(ctx context.Context, aggregateID uuid.UUID) ([]event.Event, error)
LoadStreams(ctx context.Context, ids []uuid.UUID) (map[uuid.UUID][]event.Event, error)
}
type TxManager interface {
WithinTx(ctx context.Context, fn func(ctx context.Context) error) error
}
Both backends also implement VerifyChain(ctx, aggregateID) error and the snapshot store interface.
Append takes a CommandRecord (id, type, tenant, user, idempotency key, time, JSON payload) and one StreamAppend per aggregate:
type StreamAppend struct {
AggregateID uuid.UUID
AggregateType string
PartitionKey string
ExpectedVersion int64 // last committed sequence; 0 for a new stream
Events []event.Event
UniqueKeys map[string]any
}
The command row and every stream are written atomically. The first new event must have sequence ExpectedVersion + 1.
Backends
| Backend | Use |
|---|---|
eventstore/memory |
Unit tests and integration tests. memory.New(registry). |
eventstore/postgres |
Production. postgres.Open(ctx, dsn, registry), then Migrate. Also provides TxManager and SnapshotStore. |
PostgreSQL is the only production backend. The interfaces leave room for others, but none ship.
store, err := postgres.Open(ctx, dsn, app.Events)
if err != nil { /* ... */ }
if err := store.Migrate(ctx); err != nil { /* ... */ }
app.Repo.Store = store
app.Repo.Snapshots = store
app.Service.Tx = store
Migrate is idempotent. It takes an advisory lock, records applied versions in go_sequent_schema_migrations, and applies the SQL steps 001 through 006. See Migrations and CLI.
The Postgres store takes the aggregate row lock (SELECT ... FOR NO KEY UPDATE), reads the last sequence, requires the first new event to be last plus one, and inserts. A primary key collision is a conflict. Integrity failures map to ErrOptimisticConcurrency or ErrUniqueKeyViolation.
Errors
| Error | Meaning |
|---|---|
ErrOptimisticConcurrency |
Stream tip did not match ExpectedVersion |
ErrUniqueKeyViolation |
A unique key scope and value is already used |
ErrStreamNotFound |
No events for that aggregate id |
ErrHashChainTamper |
Chain verification failed |
ErrHashChainIncomplete |
Hash columns are missing |
Test with errors.Is.
Hash chain
Integrity is a per-aggregate SHA-256 hash chain. Each stored event keeps prev_hash and event_hash. The first event (sequence 1) uses a genesis prev_hash of 32 zero bytes.
The canonical input is UTF-8 lines joined by \n, with no trailing newline:
event_typeaggregate_id(UUID string)sequence_number(decimal)created_at(UTC, truncated to microseconds, RFC3339Nano)event_json(the exact stored payload bytes)prev_hashas 64 lowercase hex characters
event_hash = SHA-256(input)
The formula lives in eventstore/hashchain. The Postgres column for the payload is BYTEA, not JSONB, so the stored bytes are the bytes that were hashed.
LoadStream verifies the chain as it reads. You can call VerifyChain(ctx, aggregateID) for an explicit check. A broken chain returns ErrHashChainTamper.
What the hash chain gives you
It detects edits, deletions and reordering inside a stream. Changing any stored field breaks every later hash.
What it does not give you
- No event signatures. The chain proves internal consistency, not who wrote an event. Someone with full database write access could recompute a whole stream. Combine it with database ACLs, encrypted backups and optional WORM storage. See Threat model.
- Per-aggregate only. There is no global chain across streams.
- A cost. Appends compute a hash and loads verify one.
Encryption of payload fields
crypto.PayloadProtector (Protect and Unprotect, with a Noop implementation) is an optional interface for encrypting selected fields of event JSON. You wire it around the event JSON in your store path; the default store does not apply it for you. It is not crypto-shredding and does not imply event authenticity.
Related
Docs track the go-sequent library. When APIs drift, the source wins.