Hanzo AI

PlanetScale

PlanetScale runs a managed database with schema branches and deploy requests. Here the database is /v1/provisioning (28) and the branch, the review and the deploy are /v1/git (44), where the migration already lives.

PlanetScale gives you a managed database, a branch of its schema, and a deploy request that puts the branch back onto production. Two capabilities answer that. /v1/provisioning (28 operations) makes the database — POST /v1/provisioning/sql launches your org's own PostgreSQL instance on port 5432 and answers with a postgres:// DSN. /v1/git (44) carries the other half, because the schema change is a file in a repo and that repo already has a branch, a proposal and a merge.

The structural difference is that one. PlanetScale hands you a second branch — inside the database, with its own host, its own password and its own deploy request number — that has to be kept in step with the git branch holding the same migration. Here there is one branch.

Start here

Mint a key, create the database, read it back until it says ready.

# 1. mint a key — sk- belongs on a server, pk- is safe in a browser
curl -sS -X POST https://api.hanzo.ai/v1/account/keys \
  -H "Authorization: Bearer $HANZO_SESSION" \
  -H 'Content-Type: application/json' \
  -d '{"type":"secret"}'

# 2. launch your org's own PostgreSQL instance — the DSN is in this answer only
curl -sS -X POST https://api.hanzo.ai/v1/provisioning/sql \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"orders"}'

# 3. read it back until status goes from provisioning to ready
curl -sS https://api.hanzo.ai/v1/provisioning/sql/orders \
  -H "Authorization: Bearer $HANZO_API_KEY"

Step 2 answers 201 with connectionString while the instance is still booting; step 3 reconciles status against the operator's live view, so ready there means psql will connect. Neither path carries an organization slug — the key does.

Core capabilities

CapabilityWhat it doesOperations
/v1/provisioningMakes the database. Seven kinds share one body and one result shape: sql, datastore, docdb, kv, search, vector, s328
/v1/gitThe branch, the pull request and the merge that carry the migration file. POST /v1/git/repos/{name}/pulls/{number}/merge is the deploy request44
/v1/o11yWhat Insights showed: POST /v1/o11y/service/top_operations for latency, call count and errors per operation381

Nouns

The database

PlanetScaleHanzo
OrganizationYour org, taken from the validated key. Never a field in the request
DatabasePOST /v1/provisioning/sql — your org's own PostgreSQL instance, port 5432
List databasesGET /v1/provisioning/sql
Database detailGET /v1/provisioning/sql/{name} — host, port, username, status, never the password
Delete a databaseDELETE /v1/provisioning/sql/{name} — a bound app reverts to Base first, then the instance goes
Connection stringconnectionString on the create result, returned once
Password with a role, per branchOne admin credential per instance, minted at create and sealed in /v1/kms (5)
Putting the credential in your appinstance in the create body — the DSN lands in that app's addons Secret
The pscale connect proxyNothing in between. The DSN is what psql and your driver take

Six other kinds sit on the same two routes: datastore for analytics, docdb on the MongoDB wire protocol, kv, search, vector and s3. Same body, same result shape, same one-name-per-resource rule.

The schema change

PlanetScaleHanzo
Branch of a databaseA branch of the repo the migration lives in — GET /v1/git/repos/{name}/refs
pscale branch push, schema applied to a branchPOST /v1/git/repos/{name}/push — files land as one commit, and it fires what a real push fires
Deploy requestA pull request — POST /v1/git/repos/{name}/pulls
Open deploy requestsGET /v1/git/repos/{name}/pulls?state=open
Deploying itPOST /v1/git/repos/{name}/pulls/{number}/merge
What landed, and whenGET /v1/git/repos/{name}/commits
Running the migrationThe build the merge fires, or POST /v1/platform/projects/{project}/apps/{app}/deploy
Build historyGET /v1/platform/builds — part of /v1/platform (37)

Insights, and the rest

PlanetScaleHanzo
Insights, per-statement p50 p95 p99POST /v1/o11y/service/top_operations — latency, call count and error count per operation
Where one slow statement sits among its peersPOST /v1/o11y/span_percentile
Slow query logGET /v1/o11y/logs
Alert when a threshold is crossed/v1/o11y/rules
Insights dashboards/v1/o11y/dashboards — part of /v1/o11y (381)
Service tokens, OAuth applications, members/v1/iam (159) — /v1/iam/service-accounts and /v1/iam/keys
Audit logGET /v1/iam/audit-logs, and GET /v1/audit (1) for the org trail
Database webhooks/v1/webhook (8) — singular, with GET /v1/webhook/{id}/deliveries
Invoices and usage/v1/billing (45) and /v1/usage (5)

The call

PlanetScale, from nothing to a deployed schema change:

curl -sS -X POST https://api.planetscale.com/v1/organizations/acme/databases \
  -H "Authorization: $PLANETSCALE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name":"orders","region":"us-east"}'

curl -sS -X POST \
  https://api.planetscale.com/v1/organizations/acme/databases/orders/branches \
  -H "Authorization: $PLANETSCALE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name":"add-line-items","parent_branch":"main"}'

curl -sS -X POST \
  https://api.planetscale.com/v1/organizations/acme/databases/orders/branches/add-line-items/passwords \
  -H "Authorization: $PLANETSCALE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name":"app","role":"readwriter"}'

curl -sS -X POST \
  https://api.planetscale.com/v1/organizations/acme/databases/orders/deploy-requests \
  -H "Authorization: $PLANETSCALE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"branch":"add-line-items","into_branch":"main"}'

curl -sS -X POST \
  https://api.planetscale.com/v1/organizations/acme/databases/orders/deploy-requests/12/deploy \
  -H "Authorization: $PLANETSCALE_TOKEN"

Five calls, and five identifiers that must agree: acme, orders, add-line-items, main, 12. A sixth is the git branch carrying the same migration, which this API never sees and cannot check.

Hanzo. The database, once:

curl -sS -X POST https://api.hanzo.ai/v1/provisioning/sql \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"orders","instance":"checkout"}'
{
  "id": "rs_7c1f",
  "kind": "sql",
  "name": "orders",
  "port": 5432,
  "status": "provisioning",
  "connectionString": "postgres://..."
}

There is no acme in that path. The store root and the instance both come from the org claim in the key, so a caller has no way to write another tenant's name down. instance binds the result: the checkout app's addons Secret receives SQL_URL, so the DSN reaches the process that needs it without a copy step. Omit instance and you keep the DSN yourself.

Then the schema change, on the branch you already pushed:

curl -sS -X POST https://api.hanzo.ai/v1/git/repos/orders/pulls \
  -H "Authorization: Bearer $HANZO_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"head":"add-line-items","base":"main","title":"add line items"}'

curl -sS -X POST https://api.hanzo.ai/v1/git/repos/orders/pulls/4/merge \
  -H "Authorization: Bearer $HANZO_API_KEY"

The merge is a fast-forward or nothing. It moves base only when base is already an ancestor of head — the case where nothing is lost and nothing is invented — and answers 409 when base has moved on independently, saying so. A deploy request is a state machine you poll through queued and in-progress; this answers with the revision base now points at. The build it fires is the same one a git push of those bytes fires, so merging is not a second road into production with its own rules. Proposing the same head into the same base twice while the first is open is a 409, so a retried run leaves one thing to review.

What does not carry

No database branch. PlanetScale forks the schema, and on a data branch the rows, into a copy with its own host and password. A name here is one instance and one DSN. A staging database is a second POST /v1/provisioning/sql under a second name, seeded by the same migrations rather than forked from production.

No deploy request, no schema diff, no revert window. PlanetScale reads your DDL, shows you the diff, applies it outside the request path and holds a window in which you can put it back. Nothing here reads your DDL. The merge moves a ref and the build runs your migration, so a bad migration is undone by another migration.

No backup or restore route. The API document has zero paths matching backup, restore or point-in-time. Restoring last night into a fresh branch, which is one of the better things PlanetScale sells, has no equivalent call. Schedule a dump on /v1/tasks (5) and land it in /v1/s3 (6).

No region, no size, no read replica. provisionRequest has exactly two fields, name and instance. There is nowhere to write us-east, ask for more memory, or add a read-only region. The instance lands in your org's tenant namespace and scales up rather than out.

No Vitess. No keyspaces, no vindexes, no horizontal sharding, and no online DDL running a ghost table behind your statement. The engine is PostgreSQL, so MySQL dialect, inline ENUM columns and SHOW statements go with it. If you are already on PlanetScale for Postgres, only the branch model changes.

The credential is returned once. connectionString and password come back at create and no read carries them again — GET /v1/provisioning/sql/{name} answers host, port, username and status, never the password — and there is no rotate route beside it. PlanetScale mints a new password whenever you ask. Lose this one and you provision again.

How is this guide?