Database
Covers the 13 API operations in the database category.
| Method | Path | Description |
|---|---|---|
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/database | Whether a database is available |
| POST | /v1/orgs/{orgSlug}/projects/{projectName}/database | Create the database (runlot deploy calls this after reading the "database": true declaration) |
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/database/connect | The information needed to connect with psql or a driver |
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/database/endpoint | Connection info without credentials (runlot port-forward) |
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/database/tokens | The list of valid short-lived credentials |
| POST | /v1/orgs/{orgSlug}/projects/{projectName}/database/tokens | Issue short-lived credentials for automation (runlot pg token) |
| DELETE | /v1/orgs/{orgSlug}/projects/{projectName}/database/tokens/{tokenUser} | Revoke short-lived credentials |
| POST | /v1/orgs/{orgSlug}/projects/{projectName}/database/export | Create one diagnostic image (runlot pg export) |
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/database/generations | The list of backup generations (runlot pg generations) |
| POST | /v1/orgs/{orgSlug}/projects/{projectName}/database/restore | Restore to a generation (runlot pg restore) |
| POST | /v1/orgs/{orgSlug}/projects/{projectName}/database/delete | Delete the database (runlot pg delete) |
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/database/operations/{opId} | The progress of one operation |
| POST | /v1/orgs/{orgSlug}/projects/{projectName}/database/operations/{opId}/abort | Abort an operation in progress (runlot pg abort) |
GET /v1/orgs/{orgSlug}/projects/{projectName}/database
The answer is a single {db: bool}. Physical state such as size and generations is a fact the node knows, so it is not here.
The credentials (the password) are not in this response. This route accepts viewer as well — connection info was split out to /database/connect.
operationId getDatabase
| Status | Description | Response body |
|---|---|---|
| 200 | Whether a database is available | object |
| 403 | — | — |
| 404 | — | — |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database
The existence of the database configuration record is what marks availability. Once enabling completes, node-agent swaps the project workerd for a configuration with env.db bound at the next convergence. No deploy or epoch change is needed.
Idempotent, and it does not roll the password. A second call re-reads the value on the row and answers with the same connection info. If it rolled, a connection string written down from the first response would silently die, and the only signal would be the next login failure.
operationId createDatabase
| Status | Description | Response body |
|---|---|---|
| 200 | Credentials and connection info | DatabaseConnect |
| 403 | — | — |
| 404 | — | — |
GET /v1/orgs/{orgSlug}/projects/{projectName}/database/connect
It is split from /database because of roles. "Is there a database" is a fact a viewer may know; the password is not. If one response carried both and fields were stripped per role, a single path that forgot to strip would be a leak. Only member or above is accepted.
operationId getDatabaseConnect
| Status | Description | Response body |
|---|---|---|
| 200 | Connection info | DatabaseConnect |
| 403 | — | — |
| 404 | The project does not exist, or the database has not been created yet | Error |
GET /v1/orgs/{orgSlug}/projects/{projectName}/database/endpoint
Only host, port, database and sslmode. It accepts viewer as well — there is no secret here. runlot port-forward attaches with the login session (the front verifies the session, docs/pg-driver-support.md §4.4), so it needs no password and this endpoint is enough.
operationId getDatabaseEndpoint
| Status | Description | Response body |
|---|---|---|
| 200 | Endpoint | object |
| 403 | — | — |
| 404 | — | — |
GET /v1/orgs/{orgSlug}/projects/{projectName}/database/tokens
The password and the verifier are not included. Requires member or above.
operationId listDatabaseTokens
| Status | Description | Response body |
|---|---|---|
| 200 | List | object |
| 403 | — | — |
| 404 | — | — |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database/tokens
One user/password pair to put in a connection string. The password exists only in this response — CP keeps only the SCRAM verifier, so if you lose it you issue a new one. The session still opens with the project's role: the token user is a name for authentication, not an engine role. Requires member or above.
operationId createDatabaseToken
Request body: application/json · object
| Status | Description | Response body |
|---|---|---|
| 201 | The issued credentials | DatabaseToken |
| 400 | — | — |
| 403 | — | — |
| 404 | — | — |
DELETE /v1/orgs/{orgSlug}/projects/{projectName}/database/tokens/{tokenUser}
It is refused from the next connection on. Deleting credentials that do not exist returns 404. Requires member or above.
operationId deleteDatabaseToken
| Status | Description | Response body |
|---|---|---|
| 204 | Revoked | — |
| 403 | — | — |
| 404 | — | — |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database/export
This is not a backup (docs/env-db-assembly.md §5). What it creates is an immutable diagnostic key under the current (project, epoch); it moves none of the latest, freshness or retention pointers, and it does not affect the name of the next automatic backup.
Scheduled backups now run (Phase 5, docs/phase5.md): node-agent owns the scheduler and the node-wide semaphore, and step generations are created on an RPO 1h grid. Those generations are stored under gen/<project>/<epoch>/; you list them at …/database/generations and pick one at …/database/restore. The diagnostic image this path creates stays outside that namespace — restore cannot pick it, pruning does not count it, and it cannot move latest.
Not idempotent. Every call creates one more image, because the server mints a new stamp per request. The only thing absorbed idempotently is a retry with the same stamp after it has reached the node.
Only member or above is accepted. The image is the whole database, not something a viewer should take away in a single call.
It is slow. The response comes after the dump finishes — proportional to size, and the project's actor is held for that whole time.
operationId exportDatabase
| Status | Description | Response body |
|---|---|---|
| 200 | The diagnostic key created | DatabaseExport |
| 403 | — | — |
| 404 | The project does not exist, or the database has not been created yet | Error |
| 409 | The placement is suspended (suspended). A diagnostic export is produced only from a running incarnation. To get data out of a suspended project, use the restore path. | Error |
| 502 | The node could not create the image (export_failed) | Error |
| 503 | Not in a state that can export — there is no home node address (no_home_node), or the node admin path is not configured on CP (no_node_admin). | Error |
GET /v1/orgs/{orgSlug}/projects/{projectName}/database/generations
Everything restore can pick (docs/phase5.md B1). It reads CP's db_generations table directly. It does not query the offsite store: bytes and sha256 have to be on CP for restore to verify the bytes it receives.
lastCheckedAt is the heartbeat of the no-change skip. Generations not increasing and backups being dead are different things, and this value separates them — empty means it has never been checked yet.
Requires member or above. The list includes the offsite key and the digest.
It does not require the database to be enabled. A final safety generation remains after pg delete, so you can still find it in this list after deletion.
operationId listGenerations
| Status | Description | Response body |
|---|---|---|
| 200 | The generation list (newest first) | DatabaseGenerations |
| 403 | — | — |
| 404 | — | — |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database/restore
Restoring a generation is a shut down the existing instance, then reactivate process (docs/phase5.md B2). The shape follows from pg.restore accepting only an empty store: draining → sealing (leave a pre-restore generation from the current data first — the way back) → committing (write the chosen generation into final_gen) → reactivation (epoch+1).
Writes made in between are lost. That is why it requires admin or above, and why the CLI asks you to type the project name again.
The server does not interpret latest. Send the (epoch, seq) you saw in the list, exactly as it was — if the server picked "the most recent", one more scheduled generation appearing in between would send you to a generation different from the one you saw.
It is a 202. What comes back is an operation id, not a result; watch progress at …/database/operations/{opId}.
operationId restoreDatabase
Request body: application/json · RestoreRequest
| Status | Description | Response body |
|---|---|---|
| 202 | The operation was opened | OperationStarted |
| 400 | — | — |
| 403 | — | — |
| 404 | The project does not exist, or the database has not been created yet | Error |
| 409 | Another operation is already in progress (operation_in_progress), or the placement is suspended (suspended). | Error |
| 503 | Not in a state that can be restored (for example, there is no placement node) | Error |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database/delete
draining → sealing (leave a final safety generation as final) → committing (delete the project_databases row; at the next convergence the node swaps in a process without env.db and moves the data directory to a tombstone, docs/phase5.md B3). Requires admin or above.
confirm must match the project name exactly. If it differs the answer is 400 confirm_mismatch and nothing starts. The CLI prompt alone is not enough — a script carrying --yes that runs in the wrong directory never shows the prompt at all.
Why this is not the DELETE method: the request does not delete one row — it starts with a 202 and finishes minutes later, and it has to take a confirmation string in the body — and every proxy along the way treats a DELETE body differently.
operationId deleteDatabase
Request body: application/json · DeleteRequest
| Status | Description | Response body |
|---|---|---|
| 202 | The operation was opened | OperationStarted |
| 400 | The confirmation string does not match the project name (confirm_mismatch) | Error |
| 403 | — | — |
| 404 | The project does not exist, or the database has not been created yet | Error |
| 409 | Another operation is already in progress (operation_in_progress) | Error |
GET /v1/orgs/{orgSlug}/projects/{projectName}/database/operations/{opId}
Watch the progress of a restore or a delete. The CLI polls every 2 seconds and prints on every phase change.
Operations live under the project because an id is not a credential. cp-core's /v1/operations/{opId} answers any client that knows the id (the node, cp-public), but on a user surface that would make a single id the key to another organization's operation state. Before answering, this route checks that the operation's projectId is the project in the path, and otherwise gives the same 404 as for something that does not exist.
Requires member or above.
operationId getDatabaseOperation
| Status | Description | Response body |
|---|---|---|
| 200 | Operation status | Operation |
| 403 | — | — |
| 404 | The project has no such operation (another project's operation lands here too) | Error |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database/operations/{opId}/abort
Possible only before the irreversible phase (docs/phase5.md B4): evict, restore_generation and delete up to committing; restore cannot be aborted. An abort returns the placement to active and leaves terminalCode=aborted.
Requires admin or above. The server reads first and aborts second — in the other order it would actually abort another project's operation and then return a 404.
operationId abortDatabaseOperation
| Status | Description | Response body |
|---|---|---|
| 200 | The state of the aborted operation | Operation |
| 403 | — | — |
| 404 | The project has no such operation | Error |
| 409 | It has passed the point of no return (not_abortable) — it is past committing, or it is a restore operation. | Error |