Database
Covers 13 API operations in the database category.
| Method | Path | Description |
|---|---|---|
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/database | Whether the database is available |
| POST | /v1/orgs/{orgSlug}/projects/{projectName}/database | Create a database (called by runlot deploy when it reads a \\"database\\": true declaration) |
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/database/connect | 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 | List of valid short-lived credentials |
| POST | /v1/orgs/{orgSlug}/projects/{projectName}/database/tokens | Issue a credential for short-lived automation (runlot pg token) |
| DELETE | /v1/orgs/{orgSlug}/projects/{projectName}/database/tokens/{tokenUser} | Revoke a short-lived credential |
| POST | /v1/orgs/{orgSlug}/projects/{projectName}/database/export | Creates a diagnostic image (runlot pg export) |
| GET | /v1/orgs/{orgSlug}/projects/{projectName}/database/generations | 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 a 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 | Aborts an in-progress operation (runlot pg abort) |
GET /v1/orgs/{orgSlug}/projects/{projectName}/database
The response is just {db: bool}. Physical state such as size and generation is not
here, since the node is the source of truth for that.
The credential (password) is not in this response. This route also accepts viewer —
connection details were split out to /database/connect.
operationId getDatabase
| Status code | Description | Response body |
|---|---|---|
| 200 | Whether the database is available | object |
| 403 | — | — |
| 404 | — | — |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database
The existence of a database configuration record indicates availability. Once the
configuration is complete, node-agent swaps the project's workerd to a configuration
with env.db connected on the next convergence. No deploy or epoch change is needed.
It is idempotent and does not rotate the password. A second call rereads the value already in the row and returns the same connection details. Rotating it would silently kill the connection string recorded from the first response, and the only signal that it died is the next login failure.
operationId createDatabase
| Status code | Description | Response body |
|---|---|---|
| 200 | Credentials and connection info | DatabaseConnect |
| 403 | — | — |
| 404 | — | — |
GET /v1/orgs/{orgSlug}/projects/{projectName}/database/connect
The reason it's split from /database is role. Whether a database exists is
something a viewer may know, but the password is not. If one response held both and
fields were stripped by role, a single path that forgot to strip would be a leak. Only
member and above can receive it.
operationId getDatabaseConnect
| Status code | Description | Response body |
|---|---|---|
| 200 | Connection info | DatabaseConnect |
| 403 | — | — |
| 404 | The project does not exist, or its database has not been created yet | Error |
GET /v1/orgs/{orgSlug}/projects/{projectName}/database/endpoint
Only host, port, database, and sslmode. Viewer access is allowed too —
there is no secret here. runlot port-forward connects using the login
session (front validates the session, docs/pg-driver-support.md §4.4), so
it needs no password, and this endpoint is all it requires.
operationId getDatabaseEndpoint
| Status code | Description | Response body |
|---|---|---|
| 200 | Endpoint | object |
| 403 | — | — |
| 404 | — | — |
GET /v1/orgs/{orgSlug}/projects/{projectName}/database/tokens
Passwords and verifiers are not included. Usable by member or above.
operationId listDatabaseTokens
| Status code | Description | Response body |
|---|---|---|
| 200 | List | object |
| 403 | — | — |
| 404 | — | — |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database/tokens
A user/password pair for use in a connection string. The password appears only in this response — CP keeps only the SCRAM verifier, so a lost password must be reissued. The session still opens under the project's role: the token user is the name used for authentication, not the engine role. member.
operationId createDatabaseToken
Request body: application/json · object
| Status code | Description | Response body |
|---|---|---|
| 201 | The issued credential | DatabaseToken |
| 400 | — | — |
| 403 | — | — |
| 404 | — | — |
DELETE /v1/orgs/{orgSlug}/projects/{projectName}/database/tokens/{tokenUser}
The next connection onward is rejected. Deleting a credential that does not exist returns 404. Available to member and above.
operationId deleteDatabaseToken
| Status code | Description | Response body |
|---|---|---|
| 204 | discarded | — |
| 403 | — | — |
| 404 | — | — |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database/export
Is not a backup (docs/env-db-assembly.md §5). What it creates is an immutable
diagnostic key under the current (project, epoch), and it moves none of the latest,
freshness, or retention pointers, nor does it affect the name of the next automatic
backup.
Scheduled backups now run (Phase 5, docs/phase5.md): node-agent owns the scheduler
and a node-wide semaphore, and step generations are produced on an RPO 1h grid. Those
generations are stored under gen/<project>/<epoch>/; you list them at
…/database/generations and select one at …/database/restore. The diagnostic image
this endpoint creates stays outside that namespace — restore cannot select it,
pruning does not count it, and it cannot move latest.
Is not idempotent. Each call creates one more image, because the server builds a new stamp for every request. The only thing absorbed idempotently is a retry of the same stamp once it has reached the node.
Requires member or higher only. The image is the entire database, so it is not something a viewer should fetch in a single call.
Is slow. The response arrives only after the dump finishes — in proportion to size — and the project's actor is held for that duration.
operationId exportDatabase
| Status code | Description | Response body |
|---|---|---|
| 200 | The diagnostic key that was created | DatabaseExport |
| 403 | — | — |
| 404 | The project does not exist, or its database has not been created yet | Error |
| 409 | The placement is suspended (suspended). Diagnostic exports are only generated from a running incarnation. To get data from a suspended project, you must use the restore path. | Error |
| 502 | The node failed to build the image (export_failed) | Error |
| 503 | Not in a state that can be exported — either there is no home node address (no_home_node), or CP has no node admin path configured (no_node_admin). | Error |
GET /v1/orgs/{orgSlug}/projects/{projectName}/database/generations
Restore can only pick from what is there (docs/phase5.md B1). It reads CP's
db_generations table directly. It does not query the offsite storage directly: bytes
and sha256 must be in CP for the restore to verify the received bytes.
lastCheckedAt is the heartbeat for the no-change skip. A generation not advancing
and a backup being dead are different things, and this value is what separates them — if
it is empty, it means no check has happened yet.
Usable by member or above. The list includes offsite keys and digests.
Does not require the database to be enabled. Because a final safety generation
remains even after pg delete, you can still see it in this list after deletion.
operationId listGenerations
| Status code | Description | Response body |
|---|---|---|
| 200 | List of generations, newest first | DatabaseGenerations |
| 403 | — | — |
| 404 | — | — |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database/restore
Restoring a generation is a process of terminating the existing instance and then
reactivating it (docs/phase5.md B2). The fact that pg.restore only accepts an empty
store dictates this shape: draining → sealing (first preserves the pre-restore
generation with the current data — a way back) → committing (writes the chosen
generation to final_gen) → reactivation (epoch+1).
Writes made in between are lost. That is why this requires admin or higher, and the CLI asks you to re-enter the project name.
The server never resolves latest. You must pass the exact (epoch, seq) you saw
in the list — if the server picked "the most recent" itself, a scheduled generation
created in the meantime could send you to a different generation than the one you saw.
Returns 202. What comes back is an operation id, not the result; check progress at
…/database/operations/{opId}.
operationId restoreDatabase
Request body: application/json · RestoreRequest
| Status code | Description | Response body |
|---|---|---|
| 202 | Opened an operation | OperationStarted |
| 400 | — | — |
| 403 | — | — |
| 404 | The project does not exist, or its database has not been created yet | Error |
| 409 | Another operation is already in progress (operation_in_progress), or the deployment is suspended (suspended). | Error |
| 503 | Not in a state that can be restored (for example, no batch node available) | Error |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database/delete
draining → sealing (leaves the final safe generation as final) →
committing (deletes the project_databases row; on the next convergence,
the node replaces the process with one that has no env.db and moves the
data directory to tombstone, docs/phase5.md B3). Requires admin or higher.
confirm must match the project name exactly. If it does not, this
returns 400 confirm_mismatch and nothing starts. A CLI prompt alone is
not enough — a script with --yes run in the wrong directory never shows
the prompt at all.
Why not the DELETE method: this request does not delete a single row; it
is an operation that starts with 202 and finishes over minutes, and it
needs a confirmation string in the body — proxies along the way handle
DELETE bodies inconsistently.
operationId deleteDatabase
Request body: application/json · DeleteRequest
| Status code | Description | Response body |
|---|---|---|
| 202 | Opened an operation | OperationStarted |
| 400 | The confirmation string does not match the project name (confirm_mismatch) | Error |
| 403 | — | — |
| 404 | The project does not exist, or its 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}
Watches the progress of a restore or delete. The CLI polls every 2 seconds and prints whenever the phase changes.
The operation lives under the project because an id is not a credential. cp-core's
/v1/operations/{opId} answers any client that knows the id (node, cp-public), but if
the user-facing surface did the same, a single id would become a key that opens another
org's operation status. This route checks that the operation's projectId matches the
project in the path before answering, and otherwise returns a 404 as if it did not
exist.
Requires member or higher.
operationId getDatabaseOperation
| Status code | Description | Response body |
|---|---|---|
| 200 | Operation status | Operation |
| 403 | — | — |
| 404 | The project has no such operation (operations belonging to other projects also land here) | Error |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database/operations/{opId}/abort
Only possible before an unrecoverable phase (docs/phase5.md B4): evict,
restore_generation, and delete are abortable up to committing; restore is not. Aborting
returns the batch to active and leaves terminalCode=aborted.
admin and above. The server reads first, then aborts — reversing the order would abort another project's operation for real before returning 404.
operationId abortDatabaseOperation
| Status code | Description | Response body |
|---|---|---|
| 200 | Status of a suspended operation | Operation |
| 403 | — | — |
| 404 | The project has no such operation | Error |
| 409 | Past the point of no return (not_abortable) — either after committing or a restore operation. | Error |