Idiomatic Go client for the ReckonDB event store, accessed over gRPC via the reckon-gateway frontend.
import "github.com/reckon-db-org/reckon-go"
c, err := reckon.Connect(ctx, "gateway.example.org:50051") // TLS, system roots (the default)
if err != nil { ... }
defer c.Close()
stores, err := c.Stores().List(ctx)reckon-go is the Go client for the Reckon event-sourcing stack. It speaks
gRPC to a running reckon-gateway,
which fronts a reckon-db event
store (embedded in the gateway, or federated across remote Erlang clusters).
You get typed, idiomatic Go over the full gateway surface without speaking
Erlang dist or hand-rolling protobuf.
The package import path ends in reckon-go, but the Go package name is
reckon (so calls read reckon.Connect(...)). One reckon.Client wraps one
gRPC connection to one gateway endpoint; per-service sub-clients are bound to a
store and share that connection:
| Sub-client | Method | Purpose |
|---|---|---|
stores |
c.Stores() |
cluster topology discovery + watch |
streams |
c.Streams(store) |
append + read events on a stream |
subscriptions |
c.Subscriptions(store) |
live + persistent subscriptions |
snapshots |
c.Snapshots(store) |
per-stream snapshots |
dcb |
c.Dcb(store) |
Dynamic Consistency Boundary writes/reads |
schema |
c.Schema(store) |
schema registration + upcasting |
temporal |
c.Temporal(store) |
wall-clock / time-travel reads |
admin |
c.Admin(store) |
scavenge, projection links, store stats |
health |
c.Health() |
gateway-wide gRPC health snapshot |
Most sub-clients take a store id and are cheap to construct (they reuse the
underlying connection). Stores() and Health() are gateway-wide and take no
store.
As a library:
go get github.com/reckon-db-org/reckon-goimport (
reckon "github.com/reckon-db-org/reckon-go"
"github.com/reckon-db-org/reckon-go/streams"
)For the reckon CLI binary, see reckon CLI below (prebuilt
binary or go install).
A full round-trip against a local gateway (plaintext): append two events to a stream, then read them back forward from version 0.
package main
import (
"context"
"fmt"
"log"
"time"
reckon "github.com/reckon-db-org/reckon-go"
"github.com/reckon-db-org/reckon-go/streams"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
// A gateway over plaintext gRPC (a local one, say). Drop reckon.Insecure() for a
// TLS gateway (the default), or use reckon.TLSWithCA for a private CA.
c, err := reckon.Connect(ctx, "localhost:50051", reckon.Insecure())
if err != nil {
log.Fatalf("connect: %v", err)
}
defer c.Close()
// my_store is the store reckon-gateway's own quick start creates; use the
// store id your gateway serves.
s := c.Streams("my_store")
// A stream id is <prefix>-<32 hex>; the store refuses any other shape.
// The rule and how to make one: https://hexdocs.pm/reckon_gater/stream_ids.html
const userID = "user-7c4b9f1e2a8d4c3b9f7e5d1a2b3c4d5e"
// Append with no concurrency check. Use streams.NoStream to assert the
// stream is new, or a non-negative version for an exact optimistic check.
res, err := s.Append(ctx, userID, streams.AnyVersion, []streams.ProposedEvent{
{EventType: "user_registered_v1", Data: []byte(`{"name":"Ada"}`)},
{EventType: "user_promoted_v1", Data: []byte(`{"role":"admin"}`)},
})
if err != nil {
log.Fatalf("append: %v", err)
}
fmt.Printf("appended: version=%d position=%d count=%d\n",
res.Version, res.Position, res.Count)
// Read forward from version 0, up to 100 events.
events, err := s.Read(ctx, userID, 0, 100)
if err != nil {
log.Fatalf("read: %v", err)
}
for _, e := range events {
fmt.Printf(" v=%-3d %-24s %s\n", e.Version, e.EventType, string(e.Data))
}
}streams.AnyVersion, streams.NoStream, and streams.StreamExists are the
optimistic-concurrency sentinels on Append; pass a non-negative
streams.ExpectedVersion for an exact-version check. Append returns an
AppendResult{Version, Position, Count}; Read returns []streams.RecordedEvent.
For a live tail use s.Watch(ctx, streamID, startVersion, count), which returns
an event channel plus an error channel.
Runnable variants of this live under examples/ (streams,
subscriptions, snapshots, admin, health), wired against the beam cluster.
For cross-cutting invariants that do not live inside a single aggregate (uniqueness, allocation, rate-limit, eligibility), use the DCB (Dynamic Consistency Boundary) sub-client instead of per-stream optimistic concurrency:
d := c.Dcb("orders")
// Read the consistency context: events matching a tag filter, plus the
// highest seq observed (MaxSeq).
rr, err := d.Read(ctx, dcb.MatchAny("slot:42"), 100)
// ...domain logic decides...
// Conditional append: commits iff no event matching the filter has a seq
// strictly above the cutoff. A non-nil *Conflict means "context stale, retry".
committed, conflict, err := d.Append(ctx, dcb.MatchAny("slot:42"), rr.MaxSeq,
[]dcb.ProposedEvent{{EventType: "slot_reserved_v1", Tags: []string{"slot:42"}}})Filters compose: dcb.MatchAny, dcb.MatchAll, dcb.EventType, dcb.And,
dcb.Or. Append returns (committed *Committed, conflict *Conflict, err error)
(exactly one of committed/conflict is non-nil when err is nil), which is
the canonical read-decide-append-retry loop (see the dcb package doc).
The DCB client also exposes CCC (Command Context Consistency) reads keyed by JSON payload field values rather than tags:
// Events where data["account_id"] == "acc-42":
events, err := d.CccReadByPayload(ctx, "account_id", "acc-42", 100)
// Events where data["flight_id"] == "FL-001" AND data["seat_no"] == "14A":
events, err := d.CccReadByPayloadHash(ctx,
[]string{"flight_id", "seat_no"},
[]string{"FL-001", "14A"},
100,
)Both return []streams.RecordedEvent. They need a {ccc, key} (single field)
or {ccc_hash, keys} (multi-field) index declared in the store config, backed
by reckon-db 5.4.0+ / reckon-gateway 0.13.1+.
reckon-go is generated against reckon-proto
and connects to a running reckon-gateway.
The committed gRPC stubs track reckon-proto minor versions; any gateway
speaking the same proto minor is compatible. Some methods need a minimum
backing store:
| Feature | Minimum backing |
|---|---|
| Core streams / subscriptions / snapshots | reckon-gateway 0.4.x, reckon-db 2.3.x |
streams.ReadByMetadata ({meta, key} index) |
reckon-db 5.0.0+ |
dcb.EventType filter ([by_event_type] index) |
reckon-db 5.2.0+ |
dcb.CccReadByPayload / CccReadByPayloadHash |
reckon-db 5.4.0+, reckon-gateway 0.13.1+ |
See Versioning for the pinning convention and CHANGELOG.md for the per-release proto mapping.
Since 0.7.0 Connect defaults to TLS verified against the system root
pool. Plaintext is an explicit opt-in, never a silent default:
// A gateway without TLS (a local one, say):
c, err := reckon.Connect(ctx, "localhost:50051", reckon.Insecure())
// Self-signed / private-CA gateway:
opt, err := reckon.TLSWithCA("/etc/reckon/ca.pem", "")
c, err := reckon.Connect(ctx, "gateway.internal:50051", opt)The CLI mirrors this: TLS by default, --plaintext for lab gateways,
--ca / --server-name for private trust anchors.
All nine gateway services wrapped. Full RPC coverage including the
catalogue-mode admin operations (Admin.ReloadCatalogue,
Admin.GetCatalogueStatus).
| Service | Wrapper status |
|---|---|
Stores |
✅ |
Streams |
✅ |
Subscriptions |
✅ |
Snapshots |
✅ |
Schema |
✅ |
Temporal |
✅ |
Admin |
✅ |
Health |
✅ |
Dcb |
✅ |
A single static binary exposing the whole API as JSON-emitting subcommands
(NDJSON for streams), for shell/CI use and as the backend for editor
integrations like reckon-nvim. See plans/DESIGN_RECKON_CLI.md.
Install without a Go toolchain (prebuilt binary from GitHub releases):
curl -fsSL https://raw.githubusercontent.com/reckon-db-org/reckon-go/main/scripts/install.sh | shInstalls reckon into ~/.local/bin (override with RECKON_BIN_DIR, pin a
release with RECKON_VERSION=vX.Y.Z). Verifies the SHA256 before installing.
(If the repo is ever made private, set RECKON_TOKEN to a GitHub read token;
install.sh then sends it as an Authorization header for both the script
fetch and the download.)
With Go (the repo is public, so this is all you need):
go install github.com/reckon-db-org/reckon-go/cmd/reckon@latestIf the repo is ever made private
go install github.com/reckon-db-org/reckon-go/cmd/reckon@latestreckon --version
reckon -e localhost:50051 --plaintext -s my_store streams read user-7c4b9f1e2a8d4c3b9f7e5d1a2b3c4d5e --count 10
reckon -e localhost:50051 --plaintext -s my_store streams watch user-7c4b9f1e2a8d4c3b9f7e5d1a2b3c4d5e # NDJSONRelease binaries (linux/{amd64,arm64}, darwin/{amd64,arm64},
windows/amd64) are built and published to GitHub releases by
.github/workflows/release.yml on every v* tag.
Generated gRPC stubs live in genproto/gatewayv1/, written by buf from the canonical proto bundle in reckon-proto:
cd ../reckon-proto && buf generate --template buf.gen.yaml --output ../reckon-goThe generated code is committed (not regenerated on go build) so consumers don't need buf installed. Regen on every proto change.
reckon-go tracks reckon-proto minor versions. SemVer at the Go-API level:
- MAJOR: breaking client API change
- MINOR: new wrappers, new functionality
- PATCH: bug fixes, behaviour unchanged
reckon-go 0.x.y against reckon-proto 0.x.* is the pinning convention while both are pre-1.0.
reckon-go is the Go client for the Reckon event-sourcing ecosystem:
- reckon-proto: the wire-contract protobufs this client is generated against.
- reckon-gateway: the gRPC + HTTP/JSON ingress this client connects to. It serves a reckon-db store (embedded or federated).
- reckon-gater: shared types and the store-worker API behind the gateway.
- reckon-db: the BEAM-native event store the gateway fronts.
- evoq + reckon-evoq: the CQRS framework and its Reckon-store adapter (server side).
- reckon-portal: docs and landing site (reckon-internal/reckon-portal).
The C4 model — who uses the module, what it reaches, and what it is made of — is in architecture/README.md, with its diagrams in assets/:
Apache-2.0.