The election was `sort.Strings(alive); return alive[0]`: the lowest-sorted
live node was the writer for EVERY tenant. Correct for SQLite, wrong for a
fleet — every org's writes land on one node, and a rolling restart hands the
whole write load to whoever sorts first next.
SQLite's single-writer constraint is per FILE, and store.Key.String()
("org/apps/a/projects/p") names one file, so ownership belongs per key.
ha.Owner answers that by Rendezvous (HRW) weight over the live set --
deterministic and order-independent, so every node reaches the same answer
from the same membership without asking anyone. Losing one of N relocates 1/N
of the tenants instead of all of them.
This also removes a dependency the fleet cannot currently satisfy.
store/multitenant.go says a tenant is pinned by "gateway-side sticky-session
affinity (consistent hash on X-Org-Id)" -- the same hash, computed at an edge
that has to be deployed and configured to agree. Asking ha.Owner in-process
needs no edge, and uses the same X-Org-Id as the key so the two models cannot
disagree.
WriterProvider now takes that key, because ownership is per tenant, not per
process. An absent X-Org-Id is the org-wide unit "" -- one owner for the whole
app, which is what every request got before, so a deployment that sets no
header behaves exactly as it does today.
The heartbeat loop no longer caches "the writer": there isn't one. It keeps
liveness and logs MEMBERSHIP changes instead, which is the more useful signal
-- membership is the input every node hashes, so disagreeing about it is the
one way two nodes can disagree about an owner.
Tests assert the PROPERTY, not the old artifact. "Lowest id wins" was a
coincidence of sorting and asserting it would forbid the spreading HRW exists
to do; the invariants are that exactly one node owns a key and every node
names the same one. Added a regression that ownership genuinely spreads across
members, which is what would catch a revert to a single pinned writer.
NOT closed, and named in the doc: heartbeat liveness cannot make a deposed
owner stop, so the rebalance overlap store/multitenant.go documents is still
real. ha.Leases + a Round the store refuses to go below is what makes it
harmless rather than merely unlikely; this election is now the shape that
accepts it.
Verified both build paths (CGO_ENABLED=0 and -tags sqlite_math_functions):
vet clean, tests pass.
Co-authored-by: Hanzo Dev <dev@hanzo.ai>
Base
The embedded, SQLite-first application backend for the Hanzo cloud — per-tenant data, in-process extension runtimes, and Hanzo IAM built in. One Go binary; the storage substrate every multi-tenant Hanzo Go service builds on.
Quick start
go install github.com/hanzoai/base/examples/base@latest
base serve
Then open the admin dashboard at http://127.0.0.1:8090/_/.
Note
Base is under active development. Full backward compatibility is not guaranteed before
v1.0.0.
What this is
Base is an open source Go backend that gives every Hanzo Go service the same multi-tenant substrate. It includes:
- embedded in-memory, SQL, and vector data with realtime subscriptions
- built-in files and users management
- a convenient Admin dashboard UI
- GraphQL and a REST-style
/v1API - native analytics and AI observability
- Hanzo IAM as the one and only auth path (OIDC / PKCE)
- deep integration with the Hanzo AI Cloud for scale on day one
Each org gets its own per-tenant data file with a per-org KMS-derived DEK; replicate streams the WAL to age-encrypted object storage. Per-record validators, computed fields, and access rules run inside the same process via in-process extension runtimes (goja, wazero, pyvm, starkvm).
Use Hanzo App to rapidly iterate and build new apps.
API clients
The easiest way to talk to the Base web API is one of the official SDK clients:
- JavaScript — @hanzoai/js-sdk (Browser, Node.js, React Native)
- Dart — @hanzoai/dart-sdk (Web, Mobile, Desktop, CLI)
See the full guide at docs.hanzo.ai.
Overview
Use as a standalone app
Download the prebuilt executable for your platform from the Releases page, extract the archive, and run ./base serve.
The prebuilt executables are built from examples/base/main.go and ship with a JavaScript plugin enabled by default, so you can extend Base with JavaScript (see Extend with JavaScript).
Use as a Go framework / toolkit
Base is distributed as a regular Go library, so you can build your own app-specific business logic and still ship a single portable executable.
Minimal example:
-
Install Go 1.26+ (if you haven't already).
-
Create a new project directory with the following
main.go:package main import ( "log" "github.com/hanzoai/base" "github.com/hanzoai/base/core" ) func main() { app := base.New() app.OnServe().BindFunc(func(se *core.ServeEvent) error { // registers a new "GET /hello" route se.Router.GET("/hello", func(re *core.RequestEvent) error { return re.String(200, "Hello world!") }) return se.Next() }) if err := app.Start(); err != nil { log.Fatal(err) } } -
Initialize dependencies:
go mod init myapp && go mod tidy. -
Start the app:
go run main.go serve. -
Build a statically linked executable:
CGO_ENABLED=0 go build, then start it with./myapp serve.
See Extend with Go for details.
Building the repo example
To build the minimal standalone executable (like the prebuilt releases), run go build inside examples/base:
- Install Go 1.26+ (if you haven't already).
- Clone the repo.
- Navigate to
examples/base. - Run
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build(see the Go environment reference). - Start the executable:
./base serve.
The pure-Go SQLite driver currently supports these build targets:
darwin amd64 linux arm64 linux s390x
darwin arm64 linux loong64 windows 386
freebsd amd64 linux ppc64le windows amd64
freebsd arm64 linux riscv64 windows arm64
linux 386 linux arm
linux amd64
Testing
Base comes with a mixed bag of unit and integration tests. Run them with the standard go test command:
go test ./...
See the Testing guide to learn how to write your own application tests.
SQLite replication
Base opens SQLite through the encrypted hanzoai/sqlite driver (per-principal CEK). The hanzoai/replicate sidecar streams WAL changes to object storage, encrypted end-to-end with luxfi/age.
K8s sidecar pattern:
- Init container — restores the latest snapshot from object storage on startup.
- Sidecar — continuously replicates the WAL while the service runs.
Configure age-identities / age-recipients for end-to-end encryption of replicated data.
Specs
Base implements:
- HIP-0105 — In-Process Extension Runtime Standard
- HIP-0107 — Streaming Replication over VFS
- HIP-0302 — Encrypted SQLite + ZapDB Durability
- HIP-0111 — Hanzo IAM (the one auth path)
It is the storage backend for every multi-tenant subsystem in HIP-0106.
Architecture
http request -> zip.App -> base.App
|
per-tenant data/{orgSlug}.db (SQLite or ZapDB)
|
KMS-derived DEK replicate -> S3 (age-encrypted)
|
in-process extension runtime (HIP-0105):
goja (JS) | wazero (WASM) | pyvm (Python) | starkvm (Starlark)
CLI
The base cli subcommand is a complete HTTP client for operating any running Base-backed daemon from the command line. It works against base, atsd, brokerd, tad, bdd, or any binary that embeds Base.
Targeting a server
# Defaults to http://127.0.0.1:8090 if nothing is set
base cli --url http://localhost:8090 collection list
# Or use environment variables
export BASE_URL=http://localhost:8090
export BASE_TOKEN=eyJhbGciOi...
base cli collection list
Global flags (apply to all subcommands):
| Flag | Env var | Default | Description |
|---|---|---|---|
--url |
BASE_URL |
http://127.0.0.1:8090 |
Server URL |
--token |
BASE_TOKEN |
~/.config/base/token |
Auth token |
--tenant |
— | — | Sets X-Org-Id header |
--format |
— | table on TTY, json otherwise |
table, json, or yaml |
Authentication
# Login as a regular user
base cli login --email user@example.com --password secret123
# Login as superuser
base cli login --email admin@example.com --password secret123 --superuser
# Check who you are
base cli whoami
The token is stored at ~/.config/base/token (or $XDG_CONFIG_HOME/base/token) with mode 0600.
Collections
# List all collections
base cli collection list
# Show a specific collection's schema
base cli collection get users
# Export schema as JSON (always JSON, ignores --format)
base cli collection schema users > schema.json
Records
# List records with filtering and sorting
base cli record list posts --filter "title~'hello'" --limit 10 --sort "-created"
# Get a single record
base cli record get posts abc123
# Create a record
base cli record create posts '{"title":"Hello","body":"World"}'
# Update a record
base cli record update posts abc123 '{"title":"Updated"}'
# Delete a record
base cli record delete posts abc123
Crons
# List registered cron schedules
base cli crons list
Using from a downstream daemon
Any Base-backed daemon can expose these subcommands without duplicating code. For example, in a downstream main.go:
package main
import (
"log"
"github.com/hanzoai/base"
"github.com/hanzoai/base/cmd"
)
func main() {
app := base.New()
// ... register domain-specific hooks and routes ...
// Register system commands for a flattened CLI:
// `ats collection list` instead of `ats cli collection list`
app.RootCmd.AddCommand(cmd.NewSuperuserCommand(app))
app.RootCmd.AddCommand(cmd.NewServeCommand(app, true))
cmd.AddCLISubcommands(app.RootCmd)
if err := app.Execute(); err != nil {
log.Fatal(err)
}
}
Then operate the running daemon:
ats collection list
ats record list trades --filter "status='settled'" --limit 20
ats crons list
ats daemon status
Registration patterns
Flattened (recommended for domain daemons) — commands at the root level:
app.RootCmd.AddCommand(cmd.NewSuperuserCommand(app))
app.RootCmd.AddCommand(cmd.NewServeCommand(app, true))
cmd.AddCLISubcommands(app.RootCmd) // collection, record, login, whoami, crons, daemon
app.Execute()
Nested (default via app.Start()) — commands under a cli parent:
app.Start() // registers serve, superuser, cli (with all subcommands)
// Access via: myapp cli collection list
Daemon lifecycle
The daemon subcommand manages the process lifecycle:
myapp daemon start # local: nohup spawn
myapp daemon stop # local: kill
myapp daemon status # local: pgrep
myapp daemon logs --follow # local: tail -f
myapp daemon restart # local: stop + start
myapp daemon status --env dev # K8s: kubectl get pods
myapp daemon restart --env test --yes # K8s: rollout restart
K8s actions require --env (dev, test, main) and are dry-run by default. Pass --yes to execute.
Security
Report security vulnerabilities to security@hanzo.ai. All reports are addressed promptly, and you'll be credited in the fix release notes.
Contributing
Base is free and open source under the MIT License.
- Open an issue for bugs and feature requests.
- Read CONTRIBUTING.md before sending a PR.
- For new features, open an issue to discuss the design first.
Bug fixes, optimizations, documentation improvements, and new OAuth2 providers are always welcome.
Hanzo — the Open AI Cloud
Open source · every language · on-chain settlement. hanzo.ai · docs.hanzo.ai
SDKs in every language — Python (flagship) · TypeScript · Go · Rust · C++ · Swift · Kotlin · umbrella