runlot
ReferenceAPI

Database

Covers 13 API operations in the database category.

MethodPathDescription
GET/v1/orgs/{orgSlug}/projects/{projectName}/databaseWhether the database is available
POST/v1/orgs/{orgSlug}/projects/{projectName}/databaseCreate a database (called by runlot deploy when it reads a \\"database\\": true declaration)
GET/v1/orgs/{orgSlug}/projects/{projectName}/database/connectInformation 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/tokensList of valid short-lived credentials
POST/v1/orgs/{orgSlug}/projects/{projectName}/database/tokensIssue 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/exportCreates a diagnostic image (runlot pg export)
GET/v1/orgs/{orgSlug}/projects/{projectName}/database/generationsList 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 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}/abortAborts 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 codeDescriptionResponse body
200Whether the database is availableobject
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 codeDescriptionResponse body
200Credentials and connection infoDatabaseConnect
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 codeDescriptionResponse body
200Connection infoDatabaseConnect
403
404The project does not exist, or its database has not been created yetError

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 codeDescriptionResponse body
200Endpointobject
403
404

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

Passwords and verifiers are not included. Usable by member or above.

operationId listDatabaseTokens

Status codeDescriptionResponse body
200Listobject
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 codeDescriptionResponse body
201The issued credentialDatabaseToken
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 codeDescriptionResponse body
204discarded
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 codeDescriptionResponse body
200The diagnostic key that was createdDatabaseExport
403
404The project does not exist, or its database has not been created yetError
409The 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
502The node failed to build the image (export_failed)Error
503Not 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 codeDescriptionResponse body
200List of generations, newest firstDatabaseGenerations
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 codeDescriptionResponse body
202Opened an operationOperationStarted
400
403
404The project does not exist, or its database has not been created yetError
409Another operation is already in progress (operation_in_progress), or the deployment is suspended (suspended).Error
503Not 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 codeDescriptionResponse body
202Opened an operationOperationStarted
400The confirmation string does not match the project name (confirm_mismatch)Error
403
404The project does not exist, or its 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}

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 codeDescriptionResponse body
200Operation statusOperation
403
404The 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 codeDescriptionResponse body
200Status of a suspended operationOperation
403
404The project has no such operationError
409Past the point of no return (not_abortable) — either after committing or a restore operation.Error

On this page