Base
Managed Hanzo Base: a hosted backend for your app — collections, records, access rules and sign-in.
Also for this capability: API · CLI · MCP · SDKs
Managed Hanzo Base: a hosted backend for your app — collections, records, access rules and sign-in.
| Base URL | https://api.hanzo.ai |
| Operations | 3 |
| Auth | Authorization: Bearer $HANZO_API_KEY |
Specification
HIP-1104 · Base — The Hosted Backend — Draft · read the specification →
/v1/base is managed Hanzo Base: a hosted backend for an app — collections,
records, access rules and sign-in — one engine instance per org, each on its own
SQLite. It also serves the platform's public waitlist at /v1/waitlist. It is
implemented in hanzoai/cloud at apps/base, embedding the
github.com/hanzoai/base engine in-process.
Motivation
The engine used to run as its own pod, whose whole job was constructing a Base
and serving it, and a third prefix forwarded to a separate managed deployment
for the sake of a cross-instance registry that could only ever answer
anonymously — it held zero rows for its whole life, while two engines answered
one question from two disks. The embed collapses that to one engine, in-process,
per org (apps/base/base.go).
Specification
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.
§1 Two lanes, deliberately not one app
Lane 1 is the waitlist: ONE platform Base instance carrying the waitlist plugin,
public because a signup surface has no principal to scope by, platform-owned
because it is one waitlist per brand rather than customer data. Lane 2 is
hosted Bases: ONE Base instance PER ORG, opened lazily and pooled. The lanes
never overlap — the waitlist plugin binds a fixed /v1/waitlist regardless of
the engine's mount prefix.
§2 The store
Per-org SQLite under {DataDir}/base/{TenantSegment}/, the platform waitlist
under a _platform segment whose leading underscore is outside the segment
alphabet so it can never collide with an org (apps/base/base.go:96-100). The
org→directory encoding is the fleet's one injective, traversal-safe tenant
encoder, so an org maps to exactly one physical identity everywhere in the
binary (apps/base/pool.go:34-40). Every store is single-open, single-writer,
on the one RWO volume, single-replica by deployment — the durability the
standalone pod had, without the pod.
§3 The pool
Instances open on first request, migrate once, and are pooled: LRU-capped at 64,
idle-evicted after 10 minutes, both env-overridable. Only IDLE instances are
ever closed — an acquired instance is pinned for the request, so eviction can
never yank one out from under a live request (apps/base/pool.go:23-31).
§4 Addresses
GET /v1/base/health is the one typed operation. Everything else under
/v1/base/* is the embedded engine's own REST surface, relayed through the
org's own mux — the engine owns that route table, and typing it in cloud would
put a second copy here, free to drift (the same shape HIP-1062 states for the
durable engine). The engine's API prefix is pinned to /v1/base so even its
self-generated URLs live under the capability's address.
§5 Tenancy and identity
The org is resolved from the validated principal and refused when absent
(apps/base/base.go:288-290); the request is then served by that org's own
instance, so cross-tenant reads are closed physically — the other org's rows are
not in the file being queried. Each per-org instance validates bearers against
Hanzo IAM's JWKS as its EXCLUSIVE auth source: the edge selects the org, Base
authorizes the record, both consume ONE IAM and no second auth path exists.
§6 Activation, money, events, telemetry, stage, upstream
The embed activates only when CLOUD_BASE_EMBED is truthy; absent it, Mount is
a health-only no-op, so linking this subsystem everywhere changes nothing until
a single-writer deployment opts in. Free (plugin/base/main.go, cloud.Free).
It publishes nothing to the bus. Beyond the request span it emits structured
log lines only. Stage ga: the hosted backend is the data plane of the
self-service core. Its upstream is github.com/hanzoai/base v1.5.65
(go.mod:691), MIT-licensed with the original author's notice preserved in its
LICENSE; the engine survives whole — this app adds the pool, the tenancy
resolution and the mount, not a fork of the engine's internals.
Rationale
The alternative to per-org files is one engine with a tenant column, which works until one query forgets the predicate. The alternative to the pool is an instance per org held open forever, which trades a cheap reopen for unbounded memory. Both alternatives were live in the estate — the forwarding prefix and the standalone pod — and both are what this design deleted.
Security Considerations
The engine is a full backend — auth, rules, file storage — so the dangerous wrong implementation is a tenancy short-circuit around it: a request routed to the wrong org's instance is a whole-backend disclosure, not a row leak. The injective segment encoding and the principal-only org resolution are the two facts that prevent it; neither takes any input a caller controls. The public waitlist lane holds no principal by design, and therefore holds no customer data — its knobs and secrets resolve from KMS at boot, never in code.
Four surfaces
| Surface | Reaches this capability as | Coverage |
|---|---|---|
| REST | base at its own prefix | 3 operations |
| CLI | hanzo base … | 3 of 3 |
| SDK | BaseApi in every published client | 3 methods |
| MCP | tool base on https://api.hanzo.ai/v1/mcp | 1 operation |
Quickstart
export HANZO_API_KEY=sk-... # console.hanzo.ai → API keysThen the first call — a read that needs nothing but the key. GET /v1/base/bases, operation get_base_bases:
hanzo base bases listimport { Configuration, BaseApi } from 'hanzoai';
const api = new BaseApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getBaseBases();from hanzoai.cloud import ApiClient, Configuration
from hanzoai.cloud.api import BaseApi
client = ApiClient(Configuration(access_token=os.environ["HANZO_API_KEY"]))
result = BaseApi(client).get_base_bases()cfg := cloud.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := cloud.NewAPIClient(cfg)
resp, _, err := client.BaseAPI.GetBaseBases(context.Background()).Execute()
if err != nil {
return err
}use hanzo_cloud::apis::{configuration::Configuration, base_api};
let mut cfg = Configuration::new();
cfg.bearer_access_token = std::env::var("HANZO_API_KEY").ok();
let result = base_api::get_base_bases(&cfg, Default::default()).await?;import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.BaseApi;
ApiClient client = new ApiClient();
client.setRequestInterceptor(b -> b.header("Authorization", "Bearer " + System.getenv("HANZO_API_KEY")));
var result = new BaseApi(client).getBaseBases();curl https://api.hanzo.ai/v1/base/bases \
-H "Authorization: Bearer $HANZO_API_KEY"MCP reaches base through the base tool, which names its 1 operations with its own verbs — this one among them, under a name only MCP declares. describe explains any of them:
curl -X POST https://api.hanzo.ai/v1/mcp \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "describe",
"arguments": {
"op": "get_base_health"
}
}
}'Answers 200 with baseView[] — ok.
Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/base/bases/{org} | Describes ONE org's Base — whether its store exists, and what it occupies. |
GET /v1/base/bases | Lists every Base the caller can reach, one per org their token carries. |
GET /v1/base/health | Reports that the base subsystem is serving. |
How is this guide?