Database
Covers 14 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 | Creates a new database after the previous one was deleted |
| 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/execute | Run one statement (the dashboard's SQL console) |
| 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}. Size and generation are not reported here.
No credentials are returned. Viewer and above may call this; connection details
live at /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
Every project has a database from the start; this creates a new, empty one after a
delete, and clears the turn-off so later deploys keep it. It returns the connection
details. Shortly afterwards
the running worker picks up a connected env.db. No redeploy is needed.
It is idempotent and never rotates the password. A second call returns the same connection details, so a connection string saved from the first response keeps working.
operationId createDatabase
| Status code | Description | Response body |
|---|---|---|
| 200 | Credentials and connection info | DatabaseConnect |
| 403 | — | — |
| 404 | — | — |
GET /v1/orgs/{orgSlug}/projects/{projectName}/database/connect
Returns the host, port, database name, user, password and sslmode needed to connect
with psql or any PostgreSQL driver. Requires member or higher — a viewer can see that
a database exists, but never its password.
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 — no secret, so viewer and above may call it.
runlot port-forward authenticates with your login session rather than a password,
so this endpoint is all it needs.
operationId getDatabaseEndpoint
| Status code | Description | Response body |
|---|---|---|
| 200 | Endpoint | object |
| 403 | — | — |
| 404 | — | — |
GET /v1/orgs/{orgSlug}/projects/{projectName}/database/tokens
Lists the project's short-lived database credentials. Passwords are never included. Requires member or higher.
operationId listDatabaseTokens
| Status code | Description | Response body |
|---|---|---|
| 200 | List | object |
| 403 | — | — |
| 404 | — | — |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database/tokens
Issues a user/password pair for use in a connection string. The password appears only in this response — only its SCRAM verifier is stored, so a lost password must be reissued. The session still opens under the project's own database role; the token user is only the login name. Requires member or higher.
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 | Revoked | — |
| 403 | — | — |
| 404 | — | — |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database/execute
Runs a single SQL statement against the project's database and returns the rows. This
backs the dashboard's SQL console; from a terminal, use runlot pg execute, which
connects directly instead.
The statement is executed over an ordinary PostgreSQL connection using the same
host, port, sslmode and credential that …/database/connect hands a member for
psql, so every rule that applies to a normal connection applies here too, including
the 10 s statement ceiling.
readOnly defaults to true, and an absent field means true. Read-only mode runs
inside BEGIN … READ ONLY and always rolls back; it never commits.
Values come back as strings or null, never JSON numbers, so large integer ids
keep their exact digits. NULL is null and the empty string is "".
At most 500 rows are listed. The cap applies to the response only and is not added
to your statement, so rowCount still reports what the statement actually did and
truncated tells you the listing was cut.
Write mode belongs to a dashboard session. A personal access token reaches this
route with db:read and runs read-only statements; asking for readOnly: false
with a token is 403 console_write_needs_session. No scope opens it — a credential
that has to write goes through …/database/connect or …/database/tokens and
connects, where the power it carries is named on the token.
Requires member or higher. A write leaves a db.execute audit event; a read leaves
none.
operationId executeSQL
Request body: application/json · ExecuteRequest
| Status code | Description | Response body |
|---|---|---|
| 200 | The statement ran | ExecuteResult |
| 400 | The database refused the statement. code is sql_error and details.sqlstate carries PostgreSQL's own five-character error code. details may also hold detail, hint and position. | Error |
| 403 | insufficient_scope — the token carries neither db:read nor a wider scope; or console_write_needs_session — readOnly: false from something other than a dashboard session. | Error |
| 404 | — | — |
| 502 | could not reach this project's database (console_unreachable) | — |
| 504 | the database did not answer within the round-trip deadline (console_timeout) | — |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database/export
This is not a backup. It creates an immutable diagnostic image of the database as it is right now. It does not move any backup pointer, does not affect retention, and cannot be selected by a restore.
Scheduled backups are separate and run on their own hourly schedule. List them at
…/database/generations and pick one at …/database/restore. Diagnostic images stay
outside that history entirely: restore cannot choose one and pruning ignores them.
This is not idempotent. Each call creates one more image.
Requires member or higher, because the image covers the whole database.
It is slow. The response arrives only once the dump has finished, and the time scales with the size of the database.
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 project is suspended (suspended). A diagnostic image can only be taken from a running project; to get data out of a suspended one, use restore instead. | Error |
| 502 | The image could not be built (export_failed) | Error |
| 503 | The project cannot be exported right now: either no process is running for it (no_home_node), or the service is temporarily unavailable (no_node_admin). | Error |
GET /v1/orgs/{orgSlug}/projects/{projectName}/database/generations
Lists the backup generations a restore can choose from, newest first, each with its
storage key, size and sha256 digest so the restore can verify what it receives.
lastCheckedAt records when the backup was last checked. A generation that is not
advancing and a backup that has stopped working are different things, and this value
tells them apart. Empty means no check has run yet.
Requires member or higher.
The database does not have to be enabled. A final safety generation is kept even
after runlot pg delete, so it still appears here after deletion.
objects is the Durable Object half of the same history, returned alongside the
database generations because a project's restorable state is the union of the two.
The two halves are timed independently, so objects.atomic is always false.
objects.restorable is objects.available && objects.complete; when it is false,
a restore will refuse outright because a component is missing or a gap was found.
restorable=true means only that the catalogue is eligible — a restore can still
fail later, for example if a node is unreachable or a stored archive is lost or
corrupt.
objects is selected at one instant for the whole project (default: now), not per
generation. A restore targets one specific (epoch, seq) generation with its own
lastAt. To find out whether the object half is eligible for that generation,
call this endpoint again with ?at=<that generation's lastAt>; the two answers can
differ.
operationId listGenerations
| Parameter | Location | Required | Type | Description |
|---|---|---|---|---|
at | query | No | string | Narrows the object half only, to the newest component at or before this instant. Database generations are always listed in full. |
| Status code | Description | Response body |
|---|---|---|
| 200 | List of generations, newest first | DatabaseGenerations |
| 403 | — | — |
| 404 | — | — |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database/restore
A restore stops the project's database, replaces its contents with the chosen generation, and starts it again. The data as it stands immediately before the restore is archived first, so there is a way back.
Any writes made after the chosen generation are lost. This requires admin or higher, and the CLI asks you to type the project name again.
If the project has Durable Objects, the same operation restores them too. Each affected object is archived and then set back to its selected component; objects placed after the restore target are removed. A missing object component refuses the whole restore, database included, rather than restoring only part of the project.
Check objects.restorable before calling this, for the generation you are about to
pass. Call …/database/generations?at=<the chosen generation's lastAt> first: the
object half is selected on its own instant, independent of the (epoch, seq) you
send here, so a restorable=true read at another instant answers a different
question. Even a correctly timed restorable=true is eligibility, not a guarantee —
the restore can still fail once it starts.
The server never resolves latest. Pass the exact (epoch, seq) you saw in the
list, so a generation created in the meantime cannot send you somewhere else.
Returns 202 with an operation id, not the result. Poll
…/database/operations/{opId} for progress.
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 project is suspended (suspended). | Error |
| 503 | The project cannot be restored right now (for example, no capacity is available) | Error |
POST /v1/orgs/{orgSlug}/projects/{projectName}/database/delete
Deletes the project's database. A final safety generation is kept, so the data can
still be listed and restored afterwards. Once the deletion completes, the worker runs
without env.db. Requires admin or higher.
This also turns end-user auth off. The subscriber records live inside the
project's database, so they go with it, and env.auth is disabled in the same step.
The stored OAuth provider secrets are removed with it and are not recoverable.
confirm must match the project name exactly. If it does not, the call returns
400 confirm_mismatch and nothing starts.
This is a POST rather than a DELETE because it is a long-running operation that
returns 202 and takes minutes, and because it carries a confirmation string in the
request body.
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 must belong to the project named in the path. An id from another project answers 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 the operation reaches a point of no return: evict,
restore_generation and delete can be aborted up to the committing phase; restore
cannot. Aborting returns the project to active and leaves terminalCode=aborted.
Requires admin or higher, and the operation must belong to the project in the path.
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 |