# PlanetScale

URL: https://docs.hanzo.ai/docs/guides/migrate/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.

```bash
# 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

| Capability         | What it does                                                                                                                                     | Operations |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------- |
| `/v1/provisioning` | Makes the database. Seven kinds share one body and one result shape: `sql`, `datastore`, `docdb`, `kv`, `search`, `vector`, `s3`                 | 28         |
| `/v1/git`          | The branch, the pull request and the merge that carry the migration file. `POST /v1/git/repos/{name}/pulls/{number}/merge` is the deploy request | 44         |
| `/v1/o11y`         | What Insights showed: `POST /v1/o11y/service/top_operations` for latency, call count and errors per operation                                    | 381        |

## Nouns

### The database

| PlanetScale                        | Hanzo                                                                                            |
| ---------------------------------- | ------------------------------------------------------------------------------------------------ |
| Organization                       | Your org, taken from the validated key. Never a field in the request                             |
| Database                           | `POST /v1/provisioning/sql` — your org's own PostgreSQL instance, port 5432                      |
| List databases                     | `GET /v1/provisioning/sql`                                                                       |
| Database detail                    | `GET /v1/provisioning/sql/{name}` — host, port, username, status, never the password             |
| Delete a database                  | `DELETE /v1/provisioning/sql/{name}` — a bound app reverts to Base first, then the instance goes |
| Connection string                  | `connectionString` on the create result, returned once                                           |
| Password with a role, per branch   | One admin credential per instance, minted at create and sealed in `/v1/kms` (5)                  |
| Putting the credential in your app | `instance` in the create body — the DSN lands in that app's addons Secret                        |
| The `pscale connect` proxy         | Nothing 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

| PlanetScale                                      | Hanzo                                                                                            |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| Branch of a database                             | A branch of the repo the migration lives in — `GET /v1/git/repos/{name}/refs`                    |
| `pscale branch push`, schema applied to a branch | `POST /v1/git/repos/{name}/push` — files land as one commit, and it fires what a real push fires |
| Deploy request                                   | A pull request — `POST /v1/git/repos/{name}/pulls`                                               |
| Open deploy requests                             | `GET /v1/git/repos/{name}/pulls?state=open`                                                      |
| Deploying it                                     | `POST /v1/git/repos/{name}/pulls/{number}/merge`                                                 |
| What landed, and when                            | `GET /v1/git/repos/{name}/commits`                                                               |
| Running the migration                            | The build the merge fires, or `POST /v1/platform/projects/{project}/apps/{app}/deploy`           |
| Build history                                    | `GET /v1/platform/builds` — part of `/v1/platform` (37)                                          |

### Insights, and the rest

| PlanetScale                                   | Hanzo                                                                                      |
| --------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Insights, per-statement p50 p95 p99           | `POST /v1/o11y/service/top_operations` — latency, call count and error count per operation |
| Where one slow statement sits among its peers | `POST /v1/o11y/span_percentile`                                                            |
| Slow query log                                | `GET /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 log                                     | `GET /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:

```bash
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:

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

```json
{
  "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:

```bash
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.
