Hanzo AI

Platform

Hanzo PaaS: deploy containers to your own tenant namespace — builds, releases, environments, logs, custom domains.

Also for this capability: API · CLI · MCP · SDKs

Hanzo PaaS: deploy containers to your own tenant namespace — builds, releases, environments, logs, custom domains.

Base URLhttps://api.hanzo.ai
Operations37
AuthAuthorization: Bearer $HANZO_API_KEY

Specification

HIP-1230 · Platform — The Container Plane — Draft · read the specification →

platform is the per-org container platform: projects, applications, builds, deploys, environments, releases, logs and verified custom domains, each application reconciled as an operator Service CR into the caller's own tenant-<org> Kubernetes namespace (apps/platform/platform.go:1-27). It is implemented in hanzoai/cloud at apps/platform. This HIP states the target surface — one address, /v1/platform — and carries two pieces previously specified apart: the forge push door (formerly HIP-1070) and the pipelines board (formerly HIP-1071), both platform addresses because their implementation is this package.

Motivation

The capability's routes grew at eight top-level addresses while its store, its process and its name were one: a generated client offered a BuildsApi, a RunnerApi and a PlatformApi for one subsystem, and one root carried a name the grammar refuses outright (git-webhook). One store is one capability however many nouns it answers for (HIP-0139 §7.1).

Specification

The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as in RFC 2119.

One store

The capability owns one store: the system-namespace platform SQLite database (apps/platform/store.go:131), holding project, application, build, deployment, environment and release rows. Tenancy is the org column on every table, enforced in every WHERE clause; the store exposes no method that reads a row without the org (apps/platform/store.go:22-28).

The addresses

Every route is under /v1/platform: apps, builds, cd, ci, environments, fleet, health, hook, pipelines, projects, releases, run and runner. /v1/platform/ci answers 501 naming what is missing — an address the fleet publishes and routes nowhere is the defect the manifest exists to prevent (manifest/apps.go:140-145). The router today still serves seven of these families at the root (/v1/builds, /v1/environments, /v1/git-webhook, /v1/pipelines, /v1/releases, /v1/run, /v1/runner); each pair is a line in cloud's openapi/misfiled.txt and folds here, with git-webhook — a hyphenated compound — renamed to hook in the same move.

Operations are typed zip operations except the push door, which cannot be: its authentication is an HMAC over the raw bytes checked before the payload is parsed, and a typed operation decodes first (apps/platform/hook.go:19-22).

The push door

POST /v1/platform/hook is where the forge delivers a push. The receiver MUST live in this process because the deploy trigger has exactly one registrant, and it is platform's: the door once lived in git's process, where that registrant is nil, so every delivery was signed, accepted, answered 204 and built nothing (apps/platform/hook.go:1-17).

The forge holds no platform session, so the address is public at the identity layer and authenticated by the HMAC signature; the bytes verified MUST be the bytes acted on, and the event kind is taken from the payload, never a header. Four bounds keep an unauthenticated door from being a lever: an encoded body is refused 415 before it is touched, a body cap bounds what is hashed, the verifying secret refreshes on a bounded window from KMS (HIP-0027), and that key-store read times out below the forge's delivery timeout. An unreadable secret answers 503, not 401 — the fault is ours. Duplicates are remembered by fact (namespace, repository, ref, commit) for a bounded window, and a failed dispatch gives the memory back. The forge half is one forge-wide system webhook signed with the value at the configured KMS reference; a repository opts in by having an application that tracks it, not by owning a hook.

The pipelines board

GET /v1/platform/pipelines is a projection, not a record: one application's build and deploy configuration joined with its latest run (apps/platform/console.go:227). The surface MUST be read-only — a pipeline comes into existence only through the one existing write path — and where an application has a latest deployment, that deployment's status replaces the configuration's. The same rule governs the sibling boards it sits beside: an environment is a scope derived from the applications that target it, a build row is a real record or an honest empty, a release is a deployment that was actually applied.

Tenancy

The org is the gateway-minted, IAM-validated claim (HIP-0026), read as c.Org(); the deploy namespace is derived from it as tenant-<org>, never taken from the request, and cross-tenant identifiers are structurally not inputs to any handler (apps/platform/platform.go:20-26). Console reads additionally require the identity to carry a user, refusing the one forgeable path — a caller reaching a pod directly with an asserted org and no bearer. Two doors differ by audience: /v1/platform/fleet is the operator's drift board, admitted only for a SuperAdmin or an org-confined OrgAdmin (apps/platform/fleet.go:31-34, HIP-0135), and the push door authenticates by signature as above. POST /v1/platform/runner — the privileged build trigger hanzo build and the push hook call — is gated by a constant-time shared token plus an image-ref allowlist confined to the registries we own (apps/platform/runner.go:1-13).

Metered

The capability is metered (plugin/platform/main.go:21), and every debit lands in integer cents through the shared cloud.ResourceMeter on the caller's org ledger:

  • build minutes — wall-clock from build creation to observed Job completion, once per completed build, at CLOUD_BUILD_MINUTE_CENTS (policy default $1.00/min; 0 makes builds free) (apps/platform/buildmeter.go:1-20);
  • running compute — each tick charges every live app's org for the span since its compare-and-set watermark, at the app's SBOM compute rate, so a double-tick or restart never double-charges (apps/platform/computemeter.go:1-30);
  • the run fee — a flat gate-then-meter-on-success debit per /v1/platform/run deploy at CLOUD_PLATFORM_RUN_FEE_CENTS (apps/platform/run.go:36).

Events, observability, stage

The capability publishes no events on the bus, so a customer's webhooks receive nothing from it. It emits lifecycle facts — push.landed, build.started, deploy.live, deploy.failed (build.go:582-585) — on the in-process stream (apps/platform/hook.go:628, apps/platform/deploy.go:290, apps/platform/applylive.go:94), fanned to registered reactors: the deploy subscriber, mirror-out, and chat notification. Beyond the request span it emits structured log lines only (apps/platform/run.go:227). Its stage is ga: the manifest row carries no stage field, and absent means ga (HIP-0139 §8).

Upstreams

The capability forks nothing. It links go-git/go-git v5 (Apache-2.0) to read pushed repositories in memory, k8s.io/apimachinery (Apache-2.0) to write operator CRs, and Masterminds/sprig v3 (MIT) for template functions. Builds execute in an in-cluster BuildKit (Apache-2.0) Job launched by image; nothing of BuildKit is linked into the binary.

Rationale

The alternative to carrying the hook and the board in this HIP is two more specification files for two things that are single routes of this package — one capability sliced into three specs, the inversion of the defect HIP-0139 §6 names.

Security Considerations

The wrong implementation hands an attacker the build plane. An unsigned or parse-before-verify hook lets an unauthenticated caller mint builds in the process that owns deploys; an encoded body accepted at that door buys megabytes of allocation for kilobytes on the wire. A deploy namespace taken from the request is a cross-tenant deploy; it is derived from the validated org instead. A leaked runner token without the image-ref allowlist pushes to an arbitrary registry; with it, only to ours. The fleet board without its admin guard is a fleet-wide rollout lever exposed to every tenant.

Four surfaces

SurfaceReaches this capability asCoverage
RESTplatform at its own prefix37 operations
CLIhanzo platform …37 of 37
SDKPlatformApi in every published client37 methods
MCPtool platform on https://api.hanzo.ai/v1/mcp39 operations, 5 under the document's own id — ask describe for the rest

Quickstart

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

Then the first call — a read that needs nothing but the key. GET /v1/platform/cd, operation get_platform_cd:

hanzo platform cd

Answers 200 with object — ok.

Endpoints

EndpointWhat it does
GET /v1/platform/apps/{app}/cdAnswers ONE app's reconciliation alone — the poll a deploy console makes while it waits, without re-reading the whole inventory each time.
GET /v1/platform/apps/{app}Answers ONE declaration — what git says this app is, before the delivery plane has had any say in it.
GET /v1/platform/appsAnswers what this organisation has declared, joined with what the delivery plane has done about it.
POST /v1/platform/appsDeploy an app through cd.hanzo.ai
GET /v1/platform/buildsReturns real build records for your org.
GET /v1/platform/cdAnswers every Application the delivery plane holds.
GET /v1/platform/ciContinuous integration (not wired)
GET /v1/platform/environmentsReturns your deploy targets, and what is running on each.
POST /v1/platform/fleet/{app}/deployRolls a platform service's pods, in a named environment.
GET /v1/platform/fleet/{app}Returns one platform service, resolved to production by default.
GET /v1/platform/fleetReturns the platform's own service tier, and where it has drifted.
GET /v1/platform/healthReports whether this control plane can actually deploy anything.
POST /v1/platform/hookReceive a push from the forge and trigger its build
GET /v1/platform/pipelinesReturns one build-and-deploy pipeline per app, with its latest run.
POST /v1/platform/projects/{project}/apps/{app}/deployDeploys the app — building it first if it comes from git.
GET /v1/platform/projects/{project}/apps/{app}/deployments/{id}/logsReturns real logs for a deployment — the build's, then the app's.
GET /v1/platform/projects/{project}/apps/{app}/deployments/{id}Returns one deployment of one app.
GET /v1/platform/projects/{project}/apps/{app}/deploymentsReturns an app's deployment history.
POST /v1/platform/projects/{project}/apps/{app}/domains/{host}/verifyChecks a custom domain's DNS and turns it on if it passes.
DELETE /v1/platform/projects/{project}/apps/{app}/domains/{host}Detaches a hostname and releases the claim.
GET /v1/platform/projects/{project}/apps/{app}/domainsReturns every hostname this app answers on.
POST /v1/platform/projects/{project}/apps/{app}/domainsAttaches a hostname — instantly if you already own it, otherwise with a DNS challenge.
PUT /v1/platform/projects/{project}/apps/{app}/envReplaces an app's environment variables.
POST /v1/platform/projects/{project}/apps/{app}/previewPuts a branch on its own URL.
POST /v1/platform/projects/{project}/apps/{app}/promotePromotes an already-built release to the app.
POST /v1/platform/projects/{project}/apps/{app}/rollbackGoes back to the previous release.
POST /v1/platform/projects/{project}/apps/{app}/startStarts a stopped app back up.
POST /v1/platform/projects/{project}/apps/{app}/stopStops an app without deleting it.
GET /v1/platform/projects/{project}/apps/{app}Returns one application, with its live phase, health and secret sync.
DELETE /v1/platform/projects/{project}/apps/{app}Deletes an application and tears down what it runs.
GET /v1/platform/projects/{project}/appsReturns the applications in one project, with what the cluster says about them.
POST /v1/platform/projects/{project}/appsCreates an application from a git repo or a container image.
GET /v1/platform/projects/{project}Returns one project and its app count.
GET /v1/platform/projectsReturns your org's projects, each with how many apps live under it.
GET /v1/platform/releasesReturns the versions that actually reached the cluster.
POST /v1/platform/runRuns a container image and gives back a URL.
POST /v1/platform/runnerTriggers a native build — an image, or the binaries a repo declares.

All Hanzo APIs · Interactive reference

How is this guide?