# Base

URL: https://docs.hanzo.ai/docs/openapi/base

> Managed Hanzo Base: a hosted backend for your app — collections, records, access rules and sign-in.

Also for this capability: **API** · [CLI](https://docs.hanzo.ai/docs/cli/base) · [MCP](https://docs.hanzo.ai/docs/mcp-tools/base) · [SDKs](https://docs.hanzo.ai/docs/sdks)

Managed Hanzo Base: a hosted backend for your app — collections, records, access rules and sign-in.

> [All API references →](https://docs.hanzo.ai/docs/openapi) · [Get started →](https://docs.hanzo.ai/docs/guides)

|                |                                        |
| -------------- | -------------------------------------- |
| **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 →](https://github.com/hanzoai/hips/blob/7ed737180339cd05b5f2935eafe7f7b83d2449a2/HIPs/hip-1104-base-hosted-backend.md)

`/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` | 3 operations, 1 under the document's own id — ask `describe` for the rest |

## Quickstart

```bash
export HANZO_API_KEY=sk-...   # console.hanzo.ai → API keys
```

Then the first call — a read that needs nothing but the key. `GET /v1/base/bases`, operation `get_base_bases`:

**CLI**

```bash
hanzo base bases list
```

**SDK**

**.ts**

```ts
import { Configuration, BaseApi } from 'hanzoai';

const api = new BaseApi(new Configuration({ accessToken: process.env.HANZO_API_KEY }));
const { data } = await api.getBaseBases();
```

**.py**

```python
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()
```

**.go**

```go
cfg := hanzoai.NewConfiguration()
cfg.AddDefaultHeader("Authorization", "Bearer "+os.Getenv("HANZO_API_KEY"))
client := hanzoai.NewAPIClient(cfg)

resp, _, err := client.BaseAPI.GetBaseBases(context.Background()).Execute()
if err != nil {
	return err
}
```

**.rs**

```rust
use hanzo_client::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?;
```

**.java**

```java
import ai.hanzo.cloud.ApiClient;
import ai.hanzo.cloud.api.BaseApi;

ApiClient client = new ApiClient();
client.setBearerToken(System.getenv("HANZO_API_KEY"));

var result = new BaseApi(client).getBaseBases();
```

**HTTP**

```bash
curl https://api.hanzo.ai/v1/base/bases \
  -H "Authorization: Bearer $HANZO_API_KEY"
```

**MCP**

MCP reaches **base** through the `base` tool, which names its 3 operations with its own verbs — this one among them, under a name only MCP declares. `describe` explains any of them:

```bash
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": "list_base_bases"
         }
       }
     }'
```

Answers `200` with `base.baseView[]` — ok.

## Endpoints

| Endpoint                                                                                    | What it does                                                               |
| ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| [`GET /v1/base/bases/{org}`](https://docs.hanzo.ai/docs/openapi/base/get-base-bases-by-org) | Describes ONE org's Base — whether its store exists, and what it occupies. |
| [`GET /v1/base/bases`](https://docs.hanzo.ai/docs/openapi/base/get-base-bases)              | Lists every Base the caller can reach, one per org their token carries.    |
| [`GET /v1/base/health`](https://docs.hanzo.ai/docs/openapi/base/get-base-health)            | Reports that the base subsystem is serving.                                |

---

[All Hanzo APIs](https://docs.hanzo.ai/docs/openapi) · [Interactive reference](https://docs.hanzo.ai/docs/openapi/reference)
