runlot
ReferenceAPI

Database

Covers the 13 API operations in the database category.

MethodPathDescription
GET/v1/orgs/{orgSlug}/projects/{projectName}/databaseWhether a database is available
POST/v1/orgs/{orgSlug}/projects/{projectName}/databaseCreate the database (runlot deploy calls this after reading the "database": true declaration)
GET/v1/orgs/{orgSlug}/projects/{projectName}/database/connectThe information needed to connect with psql or a driver
GET/v1/orgs/{orgSlug}/projects/{projectName}/database/endpointConnection info without credentials (runlot port-forward)
GET/v1/orgs/{orgSlug}/projects/{projectName}/database/tokensThe list of valid short-lived credentials
POST/v1/orgs/{orgSlug}/projects/{projectName}/database/tokensIssue 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/exportCreate one diagnostic image (runlot pg export)
GET/v1/orgs/{orgSlug}/projects/{projectName}/database/generationsThe list of backup generations (runlot pg generations)
POST/v1/orgs/{orgSlug}/projects/{projectName}/database/restoreRestore to a generation (runlot pg restore)
POST/v1/orgs/{orgSlug}/projects/{projectName}/database/deleteDelete 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}/abortAbort 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

StatusDescriptionResponse body
200Whether a database is availableobject
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

StatusDescriptionResponse body
200Credentials and connection infoDatabaseConnect
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

StatusDescriptionResponse body
200Connection infoDatabaseConnect
403
404The project does not exist, or the database has not been created yetError

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

StatusDescriptionResponse body
200Endpointobject
403
404

GET /v1/orgs/{orgSlug}/projects/{projectName}/database/tokens

The password and the verifier are not included. Requires member or above.

operationId listDatabaseTokens

StatusDescriptionResponse body
200Listobject
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

StatusDescriptionResponse body
201The issued credentialsDatabaseToken
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

StatusDescriptionResponse body
204Revoked
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

StatusDescriptionResponse body
200The diagnostic key createdDatabaseExport
403
404The project does not exist, or the database has not been created yetError
409The 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
502The node could not create the image (export_failed)Error
503Not 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

StatusDescriptionResponse body
200The 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

StatusDescriptionResponse body
202The operation was openedOperationStarted
400
403
404The project does not exist, or the database has not been created yetError
409Another operation is already in progress (operation_in_progress), or the placement is suspended (suspended).Error
503Not 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

StatusDescriptionResponse body
202The operation was openedOperationStarted
400The confirmation string does not match the project name (confirm_mismatch)Error
403
404The project does not exist, or the database has not been created yetError
409Another 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

StatusDescriptionResponse body
200Operation statusOperation
403
404The 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

StatusDescriptionResponse body
200The state of the aborted operationOperation
403
404The project has no such operationError
409It has passed the point of no return (not_abortable) — it is past committing, or it is a restore operation.Error

On this page