T4 is an open-source key-value database built on object storage.
It is designed for durable infrastructure state: config, coordination, watches, leases, transactions, recovery, and branching. T4 stores data locally for fast access, persists WAL segments and checkpoints to S3-compatible object storage, and can speak the etcd v3 API for existing infrastructure clients.
Infrastructure systems often need a small, reliable place to keep control-plane state: configuration, service metadata, locks, leases, and change notifications.
etcd is the standard answer, but operating a consensus cluster is not always the shape you want. For small platforms, edge deployments, development environments, ephemeral nodes, and systems that already trust object storage, a full Raft membership model can be more machinery than the state store deserves.
T4 explores a different tradeoff: keep the familiar key-value and watch primitives, store hot data locally, and use object storage as the durable recovery layer. Nodes can disappear, lose their disks, or be replaced; the database can rebuild from WAL segments and checkpoints in S3-compatible storage.
- Object-storage durable — WAL segments and periodic checkpoints are uploaded to S3-compatible object storage. A node that loses its disk recovers automatically.
- Infrastructure primitives — Revisions, watches, leases, transactions, and prefix scans are built in.
- Standalone or embedded — Run
t4as a server, or callt4.Open(cfg)inside a Go process. No sidecar required. - Multi-node — Leader elected via an S3 lock. Followers stream the WAL in real time and forward writes transparently.
- etcd v3 compatible — The standalone binary speaks the etcd v3 gRPC protocol, including multi-key transactions.
- Twelve-factor config — CLI flags can be supplied through
T4_*environment variables. - Branches — Fork a database at any checkpoint with zero S3 copies. Each branch writes to its own prefix; shared SST files are deduplicated automatically.
import "github.com/t4db/t4"
node, err := t4.Open(t4.Config{
DataDir: "/var/lib/myapp/t4",
})
defer node.Close()
rev, err := node.Put(ctx, "/config/timeout", []byte("30s"), 0)
kv, err := node.Get("/config/timeout")
fmt.Println(string(kv.Value)) // 30s
events, _ := node.Watch(ctx, "/config/", 0)
for e := range events {
fmt.Printf("%s %s=%s\n", e.Type, e.KV.Key, e.KV.Value)
}import (
"github.com/t4db/t4"
"github.com/t4db/t4/pkg/object"
)
store, err := object.NewS3StoreFromConfig(ctx, object.S3Config{
Bucket: "my-bucket",
Prefix: "t4/",
Region: "us-east-1",
// Endpoint: "http://localhost:9000", // MinIO or another S3-compatible store
})
if err != nil {
return err
}
node, err := t4.Open(t4.Config{
DataDir: "/var/lib/myapp/t4",
ObjectStore: store,
})The t4 binary exposes the etcd v3 gRPC protocol. Use etcdctl, the official Go client, or any other etcd v3 compatible tool.
go install github.com/t4db/t4/cmd/t4@latest
# Single node, local only
t4 run --data-dir /var/lib/t4 --listen 0.0.0.0:3379
# Single node with S3
t4 run --data-dir /var/lib/t4 --listen 0.0.0.0:3379 \
--s3-bucket my-bucket --s3-prefix t4/
# The same configuration can come from environment variables.
T4_DATA_DIR=/var/lib/t4 \
T4_LISTEN=0.0.0.0:3379 \
T4_S3_BUCKET=my-bucket \
T4_S3_PREFIX=t4/ \
T4_S3_REGION=us-east-1 \
t4 run
# Verify
etcdctl --endpoints=localhost:3379 put /hello world
etcdctl --endpoints=localhost:3379 get /helloFor offline inspection of a local data directory, use t4 inspect:
# Show local metadata without starting a server.
t4 inspect meta --data-dir /var/lib/t4
# Explore current keys.
t4 inspect list --data-dir /var/lib/t4 --prefix /config/
t4 inspect get --data-dir /var/lib/t4 /config/timeout
# Explore revision history and changes over time.
t4 inspect history --data-dir /var/lib/t4 /config/timeout
t4 inspect diff --data-dir /var/lib/t4 --from-rev 100 --to-rev 120 --prefix /config/Multi-node and production setup: see Operations.
Branches fork a database from an existing S3 checkpoint without copying shared SST files.
# Register the branch against the source prefix.
checkpoint_key=$(t4 branch fork \
--s3-bucket my-bucket \
--s3-prefix t4/ \
--branch-id experiment)
# Start the branch in its own prefix, using the source prefix as its ancestor.
t4 run \
--data-dir /var/lib/t4-experiment \
--listen 0.0.0.0:3379 \
--s3-bucket my-bucket \
--s3-prefix t4-experiment/ \
--branch-prefix t4/ \
--branch-checkpoint "$checkpoint_key"When the branch is retired, remove its registry entry so future GC can reclaim unneeded source objects:
t4 branch unfork --s3-bucket my-bucket --s3-prefix t4/ --branch-id experimentFull documentation is available at t4db.github.io/t4.
| Document | Contents |
|---|---|
| Getting Started | Quickstart for standalone server and embedded Go library |
| API Reference | Full Go API — methods, types, errors, branching |
| Configuration | All config fields and CLI flags |
| v1 Compatibility Contract | Stable API, config, object-store, WAL, and checkpoint format contracts |
| Operations | Multi-node clusters, S3, TLS, authentication, RBAC, observability |
| Backup and Restore | Checkpoints, point-in-time restore, branching, retention |
| Security | TLS, mTLS, client auth, RBAC setup |
| Recipes | Distributed locks, service discovery, common patterns |
| Kubernetes | Helm chart, StatefulSet deployment |
| Docker Compose | Local, MinIO-backed, and multi-node cluster examples |
| Architecture | Internals — WAL, checkpoints, leader election, replication |
| Benchmarks | T4 vs etcd benchmark results and analysis |
| Migrating from etcd | Compatibility table and migration steps |
| Troubleshooting | Diagnostics, debug logging, and common fixes |
| FAQ | Frequently asked questions |