# DBHost API authentication with API keys Source: https://docs.dbhost.app/api-reference/authentication Authenticate DBHost REST API and CLI requests with a Bearer API key. Choose selected-database or full-account scope on Pro and Business. ## API keys All API requests require a Bearer token in the `Authorization` header: ```bash theme={null} curl -H "Authorization: Bearer dbh_your_api_key_here" \ https://dbhost.app/api/v1/databases ``` Pro and Business use the same Bearer-token flow for the REST API and CLI. The API also enforces request shaping protections such as body-size limits and route-aware rate limiting for abusive traffic. ## Creating an API key 1. Sign in to [dbhost.app](https://dbhost.app) 2. Go to **Settings** 3. Click **Create Key** 4. Choose either `Selected databases` or `Full account access` 5. Copy the key (shown once for that secret) > [!NOTE] > > API key creation requires a Pro or Business plan. `Selected databases` is the recommended default and limits the key to only the databases you choose. `Full account access` can create and manage every database in your account. If you later need a fresh secret for the same key, use **Replace key** in Settings. The old secret stops working immediately, and the replacement secret is shown once. Selected-database keys cannot create databases. Creating a database requires a full-account key. ## Scope behavior * `GET /api/v1/databases` returns only the databases visible to the authenticated key * Database-specific routes outside the key's scope return `DATABASE_NOT_FOUND` * Full-account-only routes, such as database creation, return `KEY_SCOPE_DENIED` ## Using the CLI Install the [CLI](/cli), then save your key: ```bash theme={null} dbhost auth login --api-key dbh_your_api_key_here ``` ## Error responses | Status | Code | Description | | - | - | - | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 403 | `PLAN_API_ACCESS_REQUIRED` | The account's plan has no API access; upgrade to Pro or Business | | 403 | `KEY_SCOPE_DENIED` | Key is valid but lacks the required full-account scope | | 404 | `DATABASE_NOT_FOUND` | Database doesn't exist, isn't owned by this user, or isn't visible to this key | | 413 | `PAYLOAD_TOO_LARGE` | Request body is larger than the supported API limit | | 429 | `RATE_LIMITED` | Too many requests for the current route or too many unauthorized attempts | If you receive `429 RATE_LIMITED`, back off and retry after the number of seconds in the `Retry-After` header. # Delete backup Source: https://docs.dbhost.app/api-reference/backups/delete DELETE /api/v1/databases/{id}/backups/{filename} Delete one logical backup file of a DBHost database by filename through the API. Older S3 object versions expire under storage retention. Removes a single backup file from the available backup list. Older S3 object versions can remain until they expire under the storage retention policy; this request does not permanently purge every version. ## Path parameters | Parameter | Type | Description | | - | - | - | | `id` | uuid | Database ID | | `filename` | string | Backup filename in `YYYYMMDD_HHMMSS.sql.gz` format | ## Example ```bash theme={null} curl -X DELETE \ -H "Authorization: Bearer dbh_your_api_key_here" \ https://dbhost.app/api/v1/databases/550e8400-e29b-41d4-a716-446655440000/backups/20260326_222551.sql.gz ``` ## Response ```http theme={null} 204 No Content ``` ## Errors | Status | Code | Description | | - | - | - | | 400 | `INVALID_BACKUP_FILENAME` | Filename is not in the expected backup format | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 403 | `FORBIDDEN` | Caller can view backups but cannot manage them | | 404 | `DATABASE_NOT_FOUND` | Database doesn't exist, isn't owned by this user, or isn't in scope for this key | | 404 | `BACKUP_NOT_FOUND` | Backup file does not exist | | 503 | `AGENT_ROUTE_UNAVAILABLE` | Backup deletion is unavailable until the configured agent is updated | | 500 | `AGENT_ERROR` | The VPS agent failed to delete the backup | # Download backup Source: https://docs.dbhost.app/api-reference/backups/download GET /api/v1/databases/{id}/backups/{filename} Download one .sql.gz logical backup file of a DBHost database by filename through the API, for local storage or your own restore. Downloads a single `.sql.gz` backup file as an attachment. If you do not already know the exact filename, call [list backups](/api-reference/backups/list) first and use one of the returned `filename` values. ## Path parameters | Parameter | Type | Description | | - | - | - | | `id` | uuid | Database ID | | `filename` | string | Backup filename in `YYYYMMDD_HHMMSS.sql.gz` format | ## Example ```bash theme={null} curl -L \ -H "Authorization: Bearer dbh_your_api_key_here" \ https://dbhost.app/api/v1/databases/550e8400-e29b-41d4-a716-446655440000/backups/20260326_222551.sql.gz \ -o 20260326_222551.sql.gz ``` ## Response Returns the raw gzip file with `Content-Type: application/gzip` and `Content-Disposition: attachment`. ## Errors | Status | Code | Description | | - | - | - | | 400 | `INVALID_BACKUP_FILENAME` | Filename is not in the expected backup format | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 404 | `DATABASE_NOT_FOUND` | Database doesn't exist, isn't owned by this user, or isn't in scope for this key | | 404 | `BACKUP_NOT_FOUND` | Backup file does not exist | | 503 | `AGENT_ROUTE_UNAVAILABLE` | Backup downloads are unavailable until the configured agent is updated | | 500 | `AGENT_ERROR` | The VPS agent failed to stream the backup | # List backups Source: https://docs.dbhost.app/api-reference/backups/list GET /api/v1/databases/{id}/backups List the available logical backups for one DBHost database, newest first, with filename, backup ID, size and creation time in the response. Returns all available backups for a database, sorted newest first. ## Path parameters | Parameter | Type | Description | | - | - | - | | `id` | uuid | Database ID | ## Example ```bash theme={null} curl -H "Authorization: Bearer dbh_your_api_key_here" \ https://dbhost.app/api/v1/databases/550e8400-e29b-41d4-a716-446655440000/backups ``` ## Response ```json 200 OK theme={null} [ { "id": "7f3c2a1e-4b5d-4c6e-8f90-1a2b3c4d5e6f", "filename": "20260319_120000.sql.gz", "sizeBytes": 5242880, "sizePretty": "5.0 MB", "createdAt": "2026-03-19T12:00:00.000Z" } ] ``` Returns an empty array if no backups exist yet. `id` identifies a verified backup for the v4 and managed restore routes; it is `null` for a file that cannot be restored by ID. Use each `filename` with the [download backup](/api-reference/backups/download) and [delete backup](/api-reference/backups/delete) routes when you need to fetch or remove a specific file. ## Errors | Status | Code | Description | | - | - | - | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 404 | `DATABASE_NOT_FOUND` | Database doesn't exist, isn't owned by this user, or isn't in scope for this key | | 500 | `AGENT_ERROR` | The VPS agent failed to list backups | # Restore a single-login database from its backup Source: https://docs.dbhost.app/api-reference/backups/restore POST /api/v1/databases/{id}/backups/{filename} Restore a single-login DBHost database in place from one of its own verified backups with a staged swap, a pre-restore backup and rollback. Queues a staged restore from a verified backup of the same database. DBHost validates the backup, creates and verifies a pre-restore backup, restores into an isolated database, then swaps it in. The previous database is kept in isolation for 24 hours so DBHost can roll back if needed. The restored database keeps the connection limit and query timeout profile you had chosen: DBHost copies the database-scoped settings onto the restored database before the swap and rebinds the saved settings to the new physical database when the restore completes. > [!NOTE] > > Send an `Idempotency-Key` header and reuse it on retries. This route is for single-login databases. A database with additional roles returns `409 LEGACY_OVERLAY_REQUIRES_V4` and uses the [v4 restore](/api-reference/databases/additional-roles#restore-a-v4-backup); a database with managed SQL roles uses `POST /api/v1/databases/{id}/managed-restores`. See [Restore from a DBHost backup](/guides/restore-from-backup) for all paths. The CLI has no restore command. ## Path parameters | Parameter | Type | Description | | - | - | - | | `id` | uuid | Database ID | | `filename` | string | Backup filename (format: `YYYYMMDD_HHMMSS.sql.gz`) | ## Example ```bash theme={null} curl -X POST \ -H "Authorization: Bearer dbh_your_api_key_here" \ https://dbhost.app/api/v1/databases/550e8400-e29b-41d4-a716-446655440000/backups/20260326_222551.sql.gz ``` ## Response ```json 202 Accepted theme={null} { "message": "Staged restore accepted.", "operation": { "id": "op_01J...", "type": "restore", "status": "queued", "stage": "pre_restore_backup", "progress": 0 } } ``` Poll `GET /api/v1/operations/{id}` for validation, staging, swap, health-check, rollback, and completion status. ## Errors | Status | Code | Description | | - | - | - | | 400 | `INVALID_BACKUP_FILENAME` | Filename doesn't match the expected `YYYYMMDD_HHMMSS.sql.gz` format | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 403 | `FORBIDDEN` | The caller can view backups but cannot manage them | | 404 | `DATABASE_NOT_FOUND` | Database doesn't exist, isn't owned by this user, or isn't in scope for this key | | 404 | `FEATURE_NOT_AVAILABLE` | Restore is not available for this database type | | 404 | `BACKUP_NOT_FOUND` | Backup file not found | | 409 | `OPERATION_CONFLICT` | Another destructive operation is active for the database | | 409 | `LEGACY_OVERLAY_REQUIRES_V4` | The database has additional roles; use the v4 restore | | 409 | `SETTINGS_CHANGE_OPEN` | A connection limit or query timeout change is still settling; retry afterwards | | 503 | `AGENT_CAPABILITY_UNAVAILABLE` | Restore is temporarily unavailable; retry later | | 500 | `AGENT_ERROR` | The restore could not be started | # Uploaded dump restore is not available Source: https://docs.dbhost.app/api-reference/backups/restore-upload DBHost does not restore uploaded .sql, .sql.gz, .dump or .backup files. The routes return 409; import dumps yourself with psql or pg_restore. Customer-uploaded `.sql`, `.sql.gz`, `.dump`, and `.backup` restores are unavailable in the dashboard and public API. For an authorized database, the old upload-init, upload-complete, and direct upload routes return `409 CUSTOM_RESTORE_UPLOAD_DISABLED` without creating a restore operation. Use the database owner role and your TLS connection details to run `psql` for plain SQL or `pg_restore` for a PostgreSQL custom-format dump. See the [Restore your own dump](/guides/restore-your-own-dump) guide for examples. To restore a backup that DBHost made of the same database, see [Restore from a DBHost backup](/guides/restore-from-backup). # Trigger backup Source: https://docs.dbhost.app/api-reference/backups/trigger POST /api/v1/databases/{id}/backups Start an on-demand pg_dump backup of a DBHost database through the API. The request returns an operation that you can poll until it finishes. Triggers an asynchronous `pg_dump` backup/export. The request returns immediately while the backup runs in the background. ## Path parameters | Parameter | Type | Description | | - | - | - | | `id` | uuid | Database ID | ## Example ```bash theme={null} curl -X POST \ -H "Authorization: Bearer dbh_your_api_key_here" \ https://dbhost.app/api/v1/databases/550e8400-e29b-41d4-a716-446655440000/backups ``` ## Response ```json 202 Accepted theme={null} { "message": "Backup queued", "operation": { "id": "op_01J...", "type": "backup", "status": "queued", "stage": "queued", "progress": 0 } } ``` Poll `GET /api/v1/operations/{id}` for status. The operation ID is also used by the resulting backup artifact, audit event, and any customer webhook delivery. ## Errors | Status | Code | Description | | - | - | - | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 403 | `FORBIDDEN` | Caller can view backups but cannot manage them | | 404 | `DATABASE_NOT_FOUND` | Database doesn't exist, isn't owned by this user, or isn't in scope for this key | | 500 | `AGENT_ERROR` | The VPS agent failed to start the backup | # Database invitations API: list, resend, revoke Source: https://docs.dbhost.app/api-reference/collaboration/invitations List, resend and revoke DBHost database invitations for people without an account. Needs a full-account API key; invitations last 7 days. When you [add a member](/api-reference/collaboration/members#add-a-member) whose email does not belong to a DBHost user, DBHost emails an invitation instead. These routes let you inspect, resend and revoke those invitations. There is no separate create route. * These routes require a **full-account** API key. A selected-database key receives `403 KEY_SCOPE_DENIED`. See [API keys](/dashboard/api-keys). * The key acts as the account owner, so only databases you own are visible. * API access requires a Pro or Business plan. An invitation is valid for seven days. Statuses are `pending`, `accepted`, `revoked` and `expired`. Only `pending` invitations can be resent or revoked. ## List invitations `GET /api/v1/databases/{id}/invitations` Returns the invitations recorded for the database. Before reading, DBHost marks pending invitations whose `expiresAt` has passed as `expired`. ```http theme={null} GET /api/v1/databases/{id}/invitations Authorization: Bearer ``` ```json 200 OK theme={null} { "data": [ { "id": "b2c3d4e5-0000-4000-8000-000000000002", "email": "new@example.com", "role": "viewer", "status": "pending", "expiresAt": "2026-10-21T09:00:00.000Z", "createdAt": "2026-10-14T09:00:00.000Z" } ] } ``` The list can include invitations in every status, not only `pending`. Filter on `status` if you need open invitations only. ## Resend an invitation `POST /api/v1/databases/{id}/invitations/{invitationId}/resend` Sends a new invitation email for a pending invitation and resets its expiry to seven days from now. The invitation keeps the same `id`. DBHost then tries to revoke the link in the previous email; if that revocation fails, the old link can keep working until it expires. No request body is needed. ```http theme={null} POST /api/v1/databases/{id}/invitations/{invitationId}/resend Authorization: Bearer ``` ```json 200 OK theme={null} { "message": "Database invitation resent.", "data": { "id": "b2c3d4e5-0000-4000-8000-000000000002", "databaseId": "550e8400-e29b-41d4-a716-446655440000", "normalizedEmail": "new@example.com", "role": "viewer", "invitedByUserId": "", "clerkInvitationId": "", "status": "pending", "expiresAt": "2026-10-28T09:00:00.000Z", "acceptedByUserId": null, "acceptedAt": null, "revokedAt": null, "createdAt": "2026-10-14T09:00:00.000Z", "updatedAt": "2026-10-21T09:00:00.000Z" } } ``` ## Revoke an invitation `DELETE /api/v1/databases/{id}/invitations/{invitationId}` Revokes a pending invitation so its link can no longer be used. Returns `204 No Content` with an empty body. The invitation stays in the list with status `revoked`. ```http theme={null} DELETE /api/v1/databases/{id}/invitations/{invitationId} Authorization: Bearer ``` All changes are recorded in the database's activity log. ## Errors | Status | Code | Description | | - | - | - | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 403 | `PLAN_API_ACCESS_REQUIRED` | The account is not on Pro or Business | | 403 | `KEY_SCOPE_DENIED` | The key does not have full account access | | 404 | `DATABASE_NOT_FOUND` | Database does not exist or is not owned by this account | | 404 | `INVITATION_NOT_FOUND` | No pending invitation with this ID on this database | | 409 | `DATABASE_LIFECYCLE_CONFLICT` | The database is being deleted (resend only) | | 409 | `DATABASE_BUSY` | Another change held the database for the whole wait (resend only); retry after `Retry-After` seconds | | 429 | `RATE_LIMITED` | Too many requests; retry after `Retry-After` seconds | | 502 | `INVITATION_DELIVERY_FAILED` | The new invitation email could not be sent; the existing invitation is unchanged | | 502 | `INVITATION_REVOKE_FAILED` | The invitation could not be revoked; retry | The CLI equivalents are `dbhost invitations list ` and `dbhost invitations resend|revoke `. # Database members API: add, change role, remove Source: https://docs.dbhost.app/api-reference/collaboration/members List, add, update and remove DBHost database collaborators with a full-account API key. Roles are admin, developer and viewer. Members are people you give dashboard access to one of your databases. Roles control what they can do in the dashboard, not PostgreSQL privileges; see [Databases](/dashboard/databases#team-access) for what each role allows and [Database roles](/guides/database-roles) for SQL roles. * These routes require a **full-account** API key. A selected-database key receives `403 KEY_SCOPE_DENIED`. See [API keys](/dashboard/api-keys). * The key acts as the account owner, so only databases you own are visible. * API access requires a Pro or Business plan. Roles are `admin`, `developer` and `viewer`. The owner is listed separately and cannot be added, changed or removed through these routes. ## List members `GET /api/v1/databases/{id}/members` Returns the owner, current members and invitations for the database. ```http theme={null} GET /api/v1/databases/{id}/members Authorization: Bearer ``` ```json 200 OK theme={null} { "data": { "owner": { "id": "", "email": "owner@example.com", "name": "Owner Name", "role": "owner" }, "members": [ { "id": "a1b2c3d4-0000-4000-8000-000000000001", "userId": "", "email": "dev@example.com", "name": "Dev Name", "role": "developer", "createdAt": "2026-10-14T09:00:00.000Z" } ], "invitations": [ { "id": "b2c3d4e5-0000-4000-8000-000000000002", "email": "new@example.com", "role": "viewer", "status": "pending", "expiresAt": "2026-10-21T09:00:00.000Z", "createdAt": "2026-10-14T09:00:00.000Z" } ] } } ``` `members[].id` is the membership ID used by the update and remove routes. `invitations` has the same shape as [List invitations](/api-reference/collaboration/invitations#list-invitations). ## Add a member `POST /api/v1/databases/{id}/members` | Field | Type | Required | Description | | - | - | - | - | | `email` | string | Yes | Email address of the person to add | | `role` | string | Yes | `admin`, `developer` or `viewer` | ```http theme={null} POST /api/v1/databases/{id}/members Authorization: Bearer Content-Type: application/json {"email":"dev@example.com","role":"developer"} ``` What happens depends on whether the email belongs to an existing DBHost user. Both cases return `201 Created`; `data.kind` tells you which one applied. **Existing user.** DBHost adds the membership directly. If the user is already a member, their role is set to the requested role. ```json 201 Created theme={null} { "message": "Database member added.", "data": { "kind": "membership", "membership": { "id": "a1b2c3d4-0000-4000-8000-000000000001", "databaseId": "550e8400-e29b-41d4-a716-446655440000", "userId": "", "role": "developer", "createdByUserId": "", "createdAt": "2026-10-14T09:00:00.000Z", "updatedAt": "2026-10-14T09:00:00.000Z" } } } ``` **Unknown email.** DBHost emails an invitation that expires after seven days. An open invitation for the same email on this database is replaced by the new one. ```json 201 Created theme={null} { "message": "Database invitation sent.", "data": { "kind": "invitation", "invitation": { "id": "b2c3d4e5-0000-4000-8000-000000000002", "databaseId": "550e8400-e29b-41d4-a716-446655440000", "normalizedEmail": "new@example.com", "role": "viewer", "invitedByUserId": "", "clerkInvitationId": "", "status": "pending", "expiresAt": "2026-10-21T09:00:00.000Z", "acceptedByUserId": null, "acceptedAt": null, "revokedAt": null, "createdAt": "2026-10-14T09:00:00.000Z", "updatedAt": "2026-10-14T09:00:00.000Z" } } } ``` Manage the invitation afterwards with the [invitations routes](/api-reference/collaboration/invitations). ## Change a member's role `PATCH /api/v1/databases/{id}/members/{membershipId}` Changing the dashboard role does not rotate or revoke a shared SQL password the member may already have copied. | Field | Type | Required | Description | | - | - | - | - | | `role` | string | Yes | `admin`, `developer` or `viewer` | ```http theme={null} PATCH /api/v1/databases/{id}/members/{membershipId} Authorization: Bearer Content-Type: application/json {"role":"admin"} ``` ```json 200 OK theme={null} { "message": "Database member role updated.", "data": { "id": "a1b2c3d4-0000-4000-8000-000000000001", "databaseId": "550e8400-e29b-41d4-a716-446655440000", "userId": "", "role": "admin", "createdByUserId": "", "createdAt": "2026-10-14T09:00:00.000Z", "updatedAt": "2026-10-14T09:05:00.000Z" } } ``` ## Remove a member `DELETE /api/v1/databases/{id}/members/{membershipId}` Removes the membership and dashboard access to this database. It does not invalidate a shared SQL password the member may have copied. To stop direct SQL access, the database owner must plan a password rotation and update apps or other people that use that credential. Returns `204 No Content` with an empty body. ```http theme={null} DELETE /api/v1/databases/{id}/members/{membershipId} Authorization: Bearer ``` All changes are recorded in the database's activity log. ## Errors | Status | Code | Description | | - | - | - | | 400 | `VALIDATION_ERROR` | Missing or invalid `email` or `role`, or the email belongs to the database owner | | 400 | `INVALID_JSON` | Request body is not valid JSON | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 403 | `PLAN_API_ACCESS_REQUIRED` | The account is not on Pro or Business | | 403 | `KEY_SCOPE_DENIED` | The key does not have full account access | | 404 | `DATABASE_NOT_FOUND` | Database does not exist or is not owned by this account | | 404 | `MEMBER_NOT_FOUND` | No membership with this ID on this database | | 409 | `DATABASE_LIFECYCLE_CONFLICT` | The database is being deleted | | 409 | `DATABASE_BUSY` | Another change held the database for the whole wait; retry after `Retry-After` seconds | | 413 | `PAYLOAD_TOO_LARGE` | Request body is larger than 64 KB | | 429 | `RATE_LIMITED` | Too many requests; retry after `Retry-After` seconds | | 502 | `INVITATION_DELIVERY_FAILED` | The invitation could not be sent or saved; nothing was created | The CLI equivalents are `dbhost members list|add|update|remove`. # Access audit API: 90 days of access changes Source: https://docs.dbhost.app/api-reference/databases/access-audit Read up to 100 owner-initiated role, credential, member and network changes from the last 90 days for one DBHost database through the API. `GET /api/v1/databases/{id}/access-audit` returns up to 100 owner-initiated role, credential, member and network changes from the last 90 days for a database-scoped API key. The response contains `action`, `occurredAt`, `actor` and `outcome` only. Internal audit details, tokens, passwords, SQL and private contact data are omitted. The first customer view does not show collaborator-initiated changes or unattributed legacy events. DBHost's private platform audit remains separate from this 90-day customer view. CLI 0.3.7 or newer: `dbhost databases access-audit [--json]`. See [Connection history](/dashboard/connection-history) for the dashboard view. # Additional roles API for existing databases Source: https://docs.dbhost.app/api-reference/databases/additional-roles API for opting an existing single-login DBHost database in to additional roles, then creating, rotating, revoking and restoring with them. Any existing single-login database can opt in to additional roles. API access requires Pro or Business; the same actions are in the dashboard on every plan. See [Database roles](/guides/database-roles) for the concepts. The existing database owner, connection string and password stay in place. The API key must have access to the exact database. Responses in this flow contain role metadata; the credential endpoint is the only exception. Backups made before opt-in do not include the added roles. The older restore and clone routes refuse a database with Additional roles rather than silently losing its grants (`409 LEGACY_OVERLAY_REQUIRES_V4`). You can restore a qualified, DBHost-produced v4 backup in place through the v4 restore API. Customer SQL and dump uploads are unavailable for every database; cloning a database with Additional roles is also unavailable. The v4 restore route accepts a backup only when every non-system extension is owned by the database owner. A backup without that verified ownership is refused with `409 EXTENSION_OWNER_UNSUPPORTED`; make a new v4 backup after the ownership is corrected, or contact support if you cannot change it. ## Restore a v4 backup Use `GET /api/v1/databases/{databaseId}/backups` to find the ID of a backup made after Additional roles were enabled. The restore accepts only a DBHost-produced, S3-verified v4 backup of that same database. It replaces the current database contents with the selected backup, so writes made after the backup will no longer be present. Plan for connections to reconnect during the cutover. With a full-account API key, call `POST /api/v1/databases/{databaseId}/legacy-overlay-restores` with a stable `Idempotency-Key` header and `{"backupId":""}`. The response gives an operation ID. Call `POST /api/v1/operations/{operationId}/legacy-overlay-restore/advance` with an empty body to advance that same operation. Read `GET /api/v1/operations/{operationId}` for its current status; repeat the advance call for that operation until it reaches `succeeded` or returns a specific conflict. Reuse the same idempotency key after an uncertain reservation, and do not start a second restore to retry a stage. DBHost verifies a recovery backup before replacing the database and checks the role and grant mapping before opening the restored database. Verify your application's queries after the operation succeeds. The older generic restore route remains blocked for databases with Additional roles. Customer-uploaded restore is unavailable for every database. ## Opt in and create Call `POST /api/v1/databases/{databaseId}/legacy-roles/opt-in` with an empty body. The response contains the overlay ID, generation and `reserved` or `active` status. Repeating the request for the same database uses the same overlay. Call `POST /api/v1/databases/{databaseId}/legacy-roles` with a stable `Idempotency-Key` header and an exact body: ```json theme={null} { "displayName": "reader", "accessProfile": "read_only", "connectionLimit": 1 } ``` `accessProfile` is `custom`, `read_only` or `read_write`. A database may have up to ten extra roles. Opt-in requires an effective per-database server connection cap, and a fourth or later role rechecks it. Each role has a connection limit from 1 to 20. A successful create returns `ready` with the generated `roleId` and PostgreSQL `username`, never the password. DBHost grants database `CONNECT` for all three profiles. `custom` can log in immediately but receives no DBHost schema, table or default grants; give it access in your migrations. `read_only` gets SELECT, and `read_write` gets DML. DBHost denies CREATE and TEMPORARY to the new role. Reuse the same idempotency key after an uncertain result; changing its choices conflicts. The `prepare` and `admit` endpoints remain for a held operation that needs reconciliation. They are not needed after a successful create. If CONNECT was withdrawn later, restore it as the database owner and retry the exact admission. For an uncertain agent outcome, call `POST /api/v1/databases/{databaseId}/legacy-roles/{roleId}/reconcile` with an empty body before retrying another step. New managed databases use the same one-call behavior at `POST /api/v1/databases/{databaseId}/custom-roles`, with the same body and idempotency header. Their connection limit is a positive integer governed by the database budget. A withdrawn CONNECT grant returns `409 CONNECT_GRANT_REQUIRED` with the exact owner SQL as a fallback. ## List, credentials and changes `GET /api/v1/databases/{databaseId}/legacy-roles` lists metadata only. Use `?includeRevoked=true` to include roles that have been revoked. `POST /api/v1/databases/{databaseId}/legacy-roles/{roleId}/credentials` with an empty body explicitly reveals the ready role's current password. The response is marked `no-store`; keep the password in a secret store and do not put it in logs or an application repository. The original owner credential is never returned by this endpoint. To rotate or revoke a role, call `POST /api/v1/databases/{databaseId}/legacy-roles/{roleId}/changes` with a stable `Idempotency-Key` and `{"kind":"rotate","confirmDisconnect":true}` or `{"kind":"revoke","confirmDisconnect":true}`. Open sessions may be disconnected. Read the returned operation via `GET /api/v1/databases/{databaseId}/legacy-roles/{roleId}/changes/{operationId}`. For an uncertain outcome, replay the exact change through `POST` at that operation URL with `{"confirmDisconnect":true}`. Removing a role uses `DELETE /api/v1/databases/{databaseId}/legacy-roles/{roleId}` and refuses unresolved dependencies rather than cascading through customer objects. ## Increase a role connection limit `PUT /api/v1/databases/{databaseId}/legacy-roles/{roleId}/connection-limit` accepts `{"connectionLimit":3}` with `Idempotency-Key` for an existing ready Additional role (maximum 20). Only increases are supported; decreases return `409 ROLE_LIMIT_DECREASE_UNSUPPORTED`. The original legacy login, added role's OID, password, grants and credential generation stay unchanged. The role's server limit shares the database's aggregate cap and reserves no capacity. Accepted transactions queue within the configured client limits and wait timeout. The aggregate cap must fit the verified PostgreSQL budget, including local health and enabled direct-migration reserves. Neither the aggregate cap nor the subscription changes. A pending result retains its `operationId`; resume with `PUT` to the same URL and `{"operationId":"..."}`. `GET` returns this role's pending operation or `null`. V4 backups, same-database restore and recovery preserve the proved setting. The existing typed refusal for cloning Additional-role databases continues to apply. CLI: `dbhost managed legacy-limit DATABASE_ID ROLE_ID 3 --idempotency-key KEY`; resume with `dbhost managed legacy-continue-limit DATABASE_ID ROLE_ID OPERATION_ID`. # Connection events API: verified PgBouncer logins Source: https://docs.dbhost.app/api-reference/databases/connection-events Page through 7 days of verified PgBouncer connection events and 30 days of daily totals for one DBHost database with a scoped API key. `GET /api/v1/databases/{id}/connection-events` returns events attributed to the selected managed database by its generated role name and the signed private source. A database-scoped API key is required. Use `limit` (1–100, default 25\) and the opaque `nextCursor` to page older events. The response is `no-store` and is always restricted to that database. Raw events are retained for seven days. `kind` is one of `attempt`, `authenticated`, `auth_refused` and `closed`; an attempt does **not** prove a successful login. `tls` may be `true`, `false` or `null`. `source` is `pgbouncer_log`, and `sampled` is `false`. No SQL text, password, token, client IP or cipher is returned. The `daily` array contains outcome/TLS counts for up to 30 days. `coverage` is `unavailable`, `gap_recorded` or `no_recorded_gap`; the last value does not promise a complete connection history. Events are attributed through DBHost-generated role names, by identity rather than lifecycle state: a login made while the database was active is kept even if the database has been stopped, or deleted into its recovery window, by the time the event is delivered. An event that cannot be attributed to an immutable database ID within ten minutes of happening, or that is more than five minutes ahead of our clock, is not shown; it stays in the private source inbox and is never shown to another tenant. Direct PostgreSQL migration sessions and the older sampled-client list are separate sources. CLI 0.3.7 or newer: `dbhost databases events [--limit 1-100] [--cursor CURSOR] [--json]`. See [Connection history](/dashboard/connection-history) for the dashboard view. # Connection limits API: PgBouncer server cap Source: https://docs.dbhost.app/api-reference/databases/connection-limits Read and change a DBHost database's PgBouncer server connection cap through the API. The default and maximum come from your plan. `GET /api/v1/databases/{id}/connection-limits` reports the observed `serverConnectionCap`, `desiredServerConnectionCap`, `effectiveServerConnectionCap`, `generation`, `policyVersion`, and `operationStatus`. `allowedServerConnectionCap` gives `min`, `max` and `planMax` for this database: the default and maximum come from your plan (Free 2/4, Pro 3/12, Business 4/24), and an existing cap above `planMax` is kept as `max` until you lower it. `enforcement` is `pgbouncer_database_alias`, and `classification` is `admission_cap`. The cap is shared by all roles and PgBouncer pools for that database. It is not a hard CPU, memory, disk, or client connection quota. The readback also includes `currentServerConnections`, `waitingClients`, and `usageNearCap` from a current PgBouncer pool sample. These fields are `null` when telemetry is unavailable; zero means the alias has no active server pool. The dashboard warns at 80% of the effective server cap or when clients are waiting. This is observed usage, not a guarantee of spare CPU, memory, or disk. `preparedStatementProtocolLimit` reports the loaded PgBouncer instance setting and `preparedStatementScope` is `pgbouncer_instance_protocol`. It supports protocol-level named prepared statements across transaction pooling; the setting applies to the whole PgBouncer instance and is not a per-database control. SQL `PREPARE` surviving transaction boundaries is not promised. An account owner can request an adjustment with an API key that can write to the database: ```http theme={null} POST /api/v1/databases/{id}/connection-limits Authorization: Bearer Idempotency-Key: Content-Type: application/json {"serverConnectionCap":3} ``` Reuse the same idempotency key after a timeout or uncertain result. A pending change blocks another adjustment until its exact operation is reconciled. Lowering the cap does not disconnect existing sessions. New connections may wait for capacity. DBHost checks measured connection use on the shared host before applying an increase. | Status | Code | Meaning | | - | - | - | | 400 | `INVALID_CONNECTION_CAP_REQUEST` | The cap is not an integer from 2 to 24 | | 403 | `CONNECTION_CAP_ABOVE_PLAN` | The cap is above your plan's maximum | | 503 | `CLUSTER_CAPACITY_FULL` | The shared host is close to full; the cap is unchanged | | 409 | `CONNECTION_CAP_NOT_APPLIED` | The server did not apply the cap; the previous cap is unchanged. `error.details[0].operationId` can be read at `GET /api/v1/operations/{id}` | | 409 | `CONNECTION_CAP_*` | Another change is pending or needs reconciliation; retry with the same key | The dashboard provides the same change with one confirmation. In the CLI, use `dbhost databases limits ` and `dbhost databases limits-set --idempotency-key KEY`. See [Connection limits and timeouts](/guides/connection-limits-and-timeouts). `GET` returns `allowedServerConnectionCap` with the range you can request. Caps above 8 through the CLI need CLI 0.3.8 or newer. # Create a single-login database Source: https://docs.dbhost.app/api-reference/databases/create POST /api/v1/databases Create a single-login DBHost PostgreSQL database through the API. For separate owner, runtime and read-only roles use managed-create. Creates a new PostgreSQL database with one login (its own user and password) and a PgBouncer pool entry. This is the original single-login creation route. For separate owner, runtime and read-only roles, use [managed database creation](/api-reference/databases/managed-create). > [!TIP] > > Database names must match `^[a-z][a-z0-9-]{1,48}[a-z0-9]$` — lowercase letters, numbers, and hyphens, 3-50 characters. ## Request body | Field | Type | Required | Description | | - | - | - | - | | `name` | string | Yes | Database identifier (becomes DB name and username) | | `displayName` | string | Yes | Human-friendly label | | `environment` | string | No | `production` (default), `staging`, `development`, or `other` | | `scheduledBackup` | string | Conditional | `daily` (default for production/staging) or `none`; development/other require an explicit choice. Production requires `daily`. | ```json theme={null} { "name": "my-app", "displayName": "My Application" } ``` ## Example ```bash theme={null} curl -X POST https://dbhost.app/api/v1/databases \ -H "Authorization: Bearer dbh_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{"name":"my-app","displayName":"My Application"}' ``` ## Response ```json 201 Created theme={null} { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "my-app", "displayName": "My Application", "host": "db.dbhost.app", "port": 6432, "database": "my-app", "username": "my-app", "password": "generated-secret-token", "status": "active", "connectionString": "postgresql://my-app:generated-secret-token@db.dbhost.app:6432/my-app?sslmode=verify-full" } ``` ## Errors | Status | Code | Description | | - | - | - | | 400 | `VALIDATION_ERROR` | Request body is missing fields or the name is invalid | | 400 | `POLICY_VALIDATION` | Environment/backup choice is incomplete or unsafe | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 403 | `KEY_SCOPE_DENIED` | The API key is valid but does not have full account access | | 403 | `PLAN_API_ACCESS_REQUIRED` | API access requires a Pro or Business plan | | 403 | `PLAN_LIMIT` | Your plan does not allow more databases | | 500 | `AGENT_ERROR` | The VPS agent failed to create the database | # Additional roles API for managed databases Source: https://docs.dbhost.app/api-reference/databases/custom-roles Create, list, rename, reveal, rotate, revoke and delete up to ten additional PostgreSQL roles on a DBHost database with managed SQL roles. A database with managed SQL roles can have up to ten additional roles next to its `owner`, `runtime` and `readonly` logins. Each additional role has its own generated username, password, access profile and connection limit. See [Database roles](/guides/database-roles) for the profiles and how to grant a Custom role access. For a single-login database that opted in to additional roles, use the [additional roles API for existing databases](/api-reference/databases/additional-roles) instead. API access requires Pro or Business. Every endpoint on this page accepts a full-account key or a key scoped to selected databases that includes this database; see [Authentication](/api-reference/authentication). Every response, including errors, is sent with `Cache-Control: no-store`. Only the credentials endpoint returns a password. | Method | Path | Purpose | | - | - | - | | `GET` | `/api/v1/databases/{id}/custom-roles` | List roles | | `POST` | `/api/v1/databases/{id}/custom-roles` | Create a role | | `PATCH` | `/api/v1/databases/{id}/custom-roles/{roleId}` | Rename a role | | `POST` | `/api/v1/databases/{id}/custom-roles/{roleId}/credentials` | Reveal the password | | `POST` | `/api/v1/databases/{id}/custom-roles/{roleId}/changes` | Rotate or revoke | | `GET` | `/api/v1/databases/{id}/custom-roles/{roleId}/changes/{operationId}` | Read a change | | `POST` | `/api/v1/databases/{id}/custom-roles/{roleId}/changes/{operationId}` | Reconcile a change | | `POST` | `/api/v1/databases/{id}/custom-roles/{roleId}/delete` | Revoke and remove in one call | | `DELETE` | `/api/v1/databases/{id}/custom-roles/{roleId}` | Remove a revoked role | | `POST` | `/api/v1/databases/{id}/custom-roles/{roleId}/prepare` | Reconcile an unfinished create | | `POST` | `/api/v1/databases/{id}/custom-roles/{roleId}/admit` | Reconcile an unfinished create | `{id}` and `{roleId}` must be lowercase UUIDs. Endpoints that take no body reject a non-empty body with `400 VALIDATION_ERROR`. ## The role object ```json theme={null} { "roleId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "profile": "custom", "displayName": "reporting", "accessProfile": "read_only", "username": "dbr_550e8400e29b41d4a716446655440000_c1", "ordinal": 1, "connectionLimit": 2, "generation": 1, "credentialGeneration": 1, "status": "ready", "operationId": null } ``` | Field | Description | | - | - | | `roleId` | Role ID used in the paths on this page | | `profile` | Always `custom` for an additional role | | `displayName` | Your label for the role, 1–64 characters | | `accessProfile` | `read_only`, `read_write` or `custom` | | `username` | Generated PostgreSQL login name. It does not change on rename | | `ordinal` | Sequence number of the role on this database; not reused | | `connectionLimit` | The role's own PostgreSQL connection limit | | `generation` | Role-set generation for the database | | `credentialGeneration` | Increases by one each time the password changes | | `status` | `reserved`, `ready`, `revoked`, `updating` or `expired` | | `operationId` | List only: ID of the pending change while `status` is `updating`, otherwise `null` | `reserved` means a create has not finished; see [Reconcile an unfinished create](#reconcile-an-unfinished-create). `updating` means a rotation or revocation is pending. `expired` means the credential's validity period has ended; it can no longer be revealed or rotated. ## List roles ```http theme={null} GET /api/v1/databases/{id}/custom-roles Authorization: Bearer ``` ```json 200 OK theme={null} { "roles": [ { "roleId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "profile": "custom", "displayName": "reporting", "accessProfile": "read_only", "username": "dbr_550e8400e29b41d4a716446655440000_c1", "ordinal": 1, "connectionLimit": 2, "generation": 1, "credentialGeneration": 1, "status": "ready", "operationId": null } ] } ``` Roles are ordered by `ordinal`. Revoked roles are left out unless you add `?includeRevoked=true`. Removed roles are never listed. ## Create a role ```http theme={null} POST /api/v1/databases/{id}/custom-roles Authorization: Bearer Idempotency-Key: Content-Type: application/json { "displayName": "reporting", "accessProfile": "read_only", "connectionLimit": 2 } ``` The body must contain exactly these three fields: | Field | Rules | | - | - | | `displayName` | String, 1–64 characters after trimming, no control characters, unique among the database's roles | | `accessProfile` | `read_only`, `read_write` or `custom` | | `connectionLimit` | Integer of 1 or more, checked against the database's role connection budget | The `Idempotency-Key` header is required: 1–200 characters, without line breaks or NUL characters, and not starting with `dbhost-internal-`. A successful create finishes setup in the same call and returns the role as `ready`. The response contains no password; use [Reveal the password](#reveal-the-password). ```json 200 OK theme={null} { "roleId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "profile": "custom", "displayName": "reporting", "accessProfile": "read_only", "username": "dbr_550e8400e29b41d4a716446655440000_c1", "ordinal": 1, "connectionLimit": 2, "status": "ready" } ``` All three profiles get `CONNECT` on the database and can log in immediately. `read_only` also gets `SELECT` and `read_write` gets DML. A `custom` role gets no table grants; grant access from your migrations as the owner role. DBHost never grants an additional role `CREATE` or `TEMPORARY`. Reuse the same idempotency key after a timeout or an uncertain result. The same key with the same body returns the same role and resumes its setup; the same key with a different body returns `409 MANAGED_ROLE_CONFLICT`. Creation is refused with `409 MANAGED_ROLE_CONFLICT` when: * the database already has ten roles. Revoked roles still count; delete one to free its slot * the display name is already in use * another role on the database is still `reserved`, or a credential change on the database is unfinished * a restore of the database is in progress * the database is not active * the database would have four or more additional roles, or the new limit takes the total above the database's role budget, and the database has no effective [server connection cap](/api-reference/databases/connection-limits) If the `CONNECT` grant was withdrawn during setup, the response is `409 CONNECT_GRANT_REQUIRED` with the SQL the database owner can run, then retry the same request: ```json 409 Conflict theme={null} { "error": { "code": "CONNECT_GRANT_REQUIRED", "message": "Database owner must grant CONNECT before admitting this role.", "databaseName": "my-app", "username": "dbr_550e8400e29b41d4a716446655440000_c1", "grantSql": "GRANT CONNECT ON DATABASE \"my-app\" TO \"dbr_550e8400e29b41d4a716446655440000_c1\";" } } ``` A database cloned from a backup that predates additional-role support returns `409 CUSTOM_ROLE_REQUIRES_V3_CLONE`. Make a new clone from a newer backup of the source database before adding roles. ## Rename a role ```http theme={null} PATCH /api/v1/databases/{id}/custom-roles/{roleId} Authorization: Bearer Content-Type: application/json {"displayName":"bi-reporting"} ``` The body must contain exactly `displayName`, with the same rules as on create. Only the label changes; the username, password and grants stay the same. The role must be `ready` or `revoked`. ```json 200 OK theme={null} { "roleId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "displayName": "bi-reporting" } ``` ## Reveal the password ```http theme={null} POST /api/v1/databases/{id}/custom-roles/{roleId}/credentials Authorization: Bearer ``` Send no body. The role must be `ready`, without a pending change, and not expired. Each reveal is recorded in the account's audit log. ```json 200 OK theme={null} { "roleId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "profile": "custom", "displayName": "reporting", "accessProfile": "read_only", "username": "dbr_550e8400e29b41d4a716446655440000_c1", "ordinal": 1, "connectionLimit": 2, "generation": 1, "credentialGeneration": 1, "status": "ready", "host": "db.dbhost.app", "port": 6432, "database": "my-app", "tlsRequired": true, "password": "" } ``` Connect with TLS. Store the password in a secret manager and keep it out of logs and source control. ## Rotate or revoke ```http theme={null} POST /api/v1/databases/{id}/custom-roles/{roleId}/changes Authorization: Bearer Idempotency-Key: Content-Type: application/json {"kind":"rotate","confirmDisconnect":true} ``` The body must contain exactly `kind` (`rotate` or `revoke`) and `confirmDisconnect: true`. The `Idempotency-Key` header follows the same rules as on create. > [!WARNING] > > Rotation and revocation close the role's open connections. Open > transactions on those connections roll back. `rotate` sets a new password. `revoke` disables the login; the role keeps its slot until you delete it. The role must be `ready`, and a rotation also needs an unexpired credential. Changes are refused while a restore of the database is in progress or another credential change on the database is unfinished. ```json 202 Accepted theme={null} { "operationId": "22222222-2222-4222-8222-222222222222", "roleId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "profile": "custom", "kind": "rotate", "status": "pending", "credentialGeneration": 2 } ``` The status code is `202` while `status` is `pending` and `200` when it is `completed`. `credentialGeneration` is the generation the change produces. Retry with the same key and body after an uncertain result; a different `kind` on the same key is refused. ### Read a change ```http theme={null} GET /api/v1/databases/{id}/custom-roles/{roleId}/changes/{operationId} Authorization: Bearer ``` Returns `200` with the same fields as above. It does not advance the change. ### Reconcile a change ```http theme={null} POST /api/v1/databases/{id}/custom-roles/{roleId}/changes/{operationId} Authorization: Bearer Content-Type: application/json {"confirmDisconnect":true} ``` The body must be exactly `{"confirmDisconnect": true}`. DBHost replays the recorded change and returns `202` while pending or `200` when completed. A change that has already completed is not applied again. ## Delete a role ```http theme={null} POST /api/v1/databases/{id}/custom-roles/{roleId}/delete Authorization: Bearer Content-Type: application/json {"confirm":true} ``` The body must be exactly `{"confirm": true}`. A `ready` role is revoked first, which closes its open connections, then removed from PostgreSQL. A `revoked` role is removed directly. Repeat the same call to continue a pending delete. | Status | Body | Meaning | | - | - | - | | 200 | `{"roleId": "...", "status": "removed"}` | The role is gone and its slot is free | | 202 | `{"roleId": "...", "status": "pending", "phase": "revoke", "operationId": "..."}` | Revocation is still running | | 202 | `{"roleId": "...", "status": "pending", "phase": "remove"}` | Removal is still running | DBHost does not cascade through your objects. If the role still owns objects or holds grants, the role stays revoked and the response lists what to clean up: ```json 409 Conflict theme={null} { "error": { "code": "ROLE_DEPENDENCIES", "message": "Remove the listed customer grants or objects before retrying", "dependencies": [ { "kind": "grant", "objectType": "table", "schema": "public", "name": "orders", "identity": "table public.orders" } ], "truncated": false } } ``` `kind` is `grant`, `owned_object`, `policy`, `other` or `other_database`. At most 20 entries are returned; `truncated: true` means there are more. Revoke the grants or reassign the objects as the owner role, then call delete again. ### Remove a revoked role ```http theme={null} DELETE /api/v1/databases/{id}/custom-roles/{roleId} Authorization: Bearer ``` Send no body. This removes a role that is already `revoked` and returns `200` with `{"roleId": "...", "status": "removed"}`, or `202` with `{"roleId": "...", "status": "pending"}` while removal runs. Remaining dependencies return `409 ROLE_DEPENDENCIES` without the list; use the `/delete` endpoint above to see it. ## Reconcile an unfinished create A create that was interrupted can leave a role in `reserved`. Retrying the original create with the same idempotency key and body is the simplest way to finish it. The two step endpoints are also available. Both take no body. `POST /api/v1/databases/{id}/custom-roles/{roleId}/prepare` creates the PostgreSQL role and returns `202`: ```json 202 Accepted theme={null} { "roleId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "profile": "custom", "username": "dbr_550e8400e29b41d4a716446655440000_c1", "displayName": "reporting", "accessProfile": "read_only", "ordinal": 1, "status": "sql_ready" } ``` `POST /api/v1/databases/{id}/custom-roles/{roleId}/admit` applies the profile's grants and opens the login. It returns `200` with the same fields and `"status": "ready"`, or `409 CONNECT_GRANT_REQUIRED` with `grantSql` as shown under [Create a role](#create-a-role). Neither step is needed after a successful create. ## Errors | Status | Code | Description | | - | - | - | | 400 | `VALIDATION_ERROR` | Invalid ID, unexpected or missing body field, missing confirmation, or missing or invalid `Idempotency-Key` | | 400 | `INVALID_JSON` | Request body is not valid JSON | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 403 | `PLAN_API_ACCESS_REQUIRED` | API access requires Pro or Business | | 404 | `DATABASE_NOT_FOUND` | Database doesn't exist, isn't owned by this account or isn't in the key's scope | | 409 | `MANAGED_ROLE_CONFLICT` | The role or database is not in a state that allows this action, or the idempotency key was used with a different request | | 409 | `CONNECT_GRANT_REQUIRED` | The role needs `CONNECT`; run `grantSql` as the owner and retry | | 409 | `CUSTOM_ROLE_REQUIRES_V3_CLONE` | The database is a clone from an older backup; create a new clone first | | 409 | `ROLE_DEPENDENCIES` | The role still has grants or owns objects | | 413 | `PAYLOAD_TOO_LARGE` | Request body is larger than the API limit | | 429 | `RATE_LIMITED` | Too many requests; retry after the `Retry-After` header | | 503 | `FEATURE_DISABLED` | Additional roles are temporarily unavailable | | 503 | `MAINTENANCE` | DBHost is in a maintenance window; retry later | | 500 | `INTERNAL_ERROR` | The request failed; retry the same request | ## Dashboard and CLI The same actions are on the database detail page under **Additional roles** on every plan. The CLI provides them as `dbhost managed custom-*` commands; see [CLI](/cli). ## Increase an existing connection limit `PUT /api/v1/databases/{databaseId}/custom-roles/{roleId}/connection-limit` with `Idempotency-Key` and an exact body `{"connectionLimit":3}` increases an existing ready role's server connection limit. The role, password, grants and credential generation are preserved. Decreases return `409 ROLE_LIMIT_DECREASE_UNSUPPORTED`. The role limit is an upper bound within the database's shared pooler cap, not reserved capacity. Accepted client transactions queue when the role's server connections are busy, subject to the existing client limits and wait timeout. The database cap and plan are not changed by this operation. The shared cap must fit the verified PostgreSQL budget, including local health and enabled direct-migration reserves; otherwise the increase is refused before reservation. A completed result has `status: "completed"`. A `202` / `status: "pending"` result retains its `operationId`. Resume that exact operation with `PUT` to the same URL and `{"operationId":"..."}`. `GET` on the URL returns an outstanding change for this role, or `null`; reading does not apply changes. A pending outcome blocks conflicting database/role mutations until reconciled. CLI: `dbhost managed custom-limit DATABASE_ID ROLE_ID 3 --idempotency-key KEY`. Resume with `dbhost managed custom-continue-limit DATABASE_ID ROLE_ID OPERATION_ID`. New backups include the proved limit. Same-database restore retains the current role setting; clones use the qualified backup policy, and recovery retains the deleted database's proved setting. Historical receipts are never overwritten. # Delete database Source: https://docs.dbhost.app/api-reference/databases/delete DELETE /api/v1/databases/{id} Delete a DBHost database through the API. DBHost verifies a deletion backup first, and the database can be recovered for seven days. Queues guarded database deletion. The request must confirm the exact internal database name. DBHost creates and verifies a deletion backup, removes routing, and creates a tombstone with a seven-day self-service recovery window. The tombstone does not consume active database quota but reserves the name. After seven days, DBHost permanently deletes the database and its deletion backup automatically. > [!WARNING] > > Deletion requires an API key with access to the database, the exact > database name and a verified deletion backup. Protected or unsupported > targets receive `FEATURE_NOT_AVAILABLE`; a name alone is never enough. ## Path parameters | Parameter | Type | Description | | - | - | - | | `id` | uuid | Canonical database ID | ## Request ```json theme={null} { "confirmation": "exact_internal_database_name" } ``` The operation validates ownership, the database ID and name, and the permanent protected-target list before anything is removed. ## Response ```json 202 Accepted theme={null} { "message": "Database deletion queued", "operation": { "id": "op_01J...", "type": "delete", "status": "queued", "stage": "verify_deletion_backup", "progress": 0 } } ``` Poll `GET /api/v1/operations/{id}`. After success, use the deleted-databases endpoint to inspect the recovery deadline or queue recovery while quota permits. ## Errors | Status | Code | Description | | - | - | - | | 400 | `CONFIRMATION_REQUIRED` | Typed name does not match the bound database | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 404 | `DATABASE_NOT_FOUND` | Database is missing or outside key scope | | 404 | `FEATURE_NOT_AVAILABLE` | The target cannot be deleted through the API | | 409 | `DATABASE_STOPPED` | Start the database first; deletion backs it up | | 409 | `OPERATION_CONFLICT` | Another destructive operation is active | | 409 | `BACKUP_NOT_VERIFIED` | Required deletion backup is not verified | | 503 | `AGENT_CAPABILITY_UNAVAILABLE` | Deletion is temporarily unavailable; retry later | # Deleted databases: list, download, recover Source: https://docs.dbhost.app/api-reference/databases/deleted List deleted DBHost databases, download their deletion backup and recover them within seven days through the API with a full-account key. Deleted-database lifecycle endpoints require a full-account API key. ## List recently deleted databases `GET /api/v1/databases/deleted` Returns unpurged tombstones owned by the API-key owner. Each item includes `deletedAt`, `purgeAfter`, `recoveryStatus` and metadata for the exact verified deletion backup: ```json theme={null} { "data": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "my-app", "displayName": "My Application", "deletedAt": "2026-07-27T09:00:00.000Z", "purgeAfter": "2026-08-03T09:00:00.000Z", "recoveryStatus": "recoverable", "deletionBackup": { "fileName": "20260727_090000_000000.sql.gz", "format": "sql.gz", "sizeBytes": 4194304, "checksumSha256": "", "verifiedAt": "2026-07-27T09:01:00.000Z", "downloadUrl": "/api/v1/databases/550e8400-e29b-41d4-a716-446655440000/deletion-backup" } } ] } ``` `recoveryStatus` is `recoverable`, `unavailable`, or `expired`. An incomplete backup or access-snapshot binding is reported as unavailable rather than being offered for recovery. `deletionBackup.downloadUrl` is `null` after `purgeAfter`, even while metadata remains visible during a short purge retry. ## Download the deletion backup `GET /api/v1/databases/{id}/deletion-backup` Returns the exact operation-bound deletion artifact as a download or short-lived redirect. The caller cannot select a filename. The route returns `404 DELETION_BACKUP_NOT_AVAILABLE` after expiry, purge, ownership mismatch or any database, operation, backup, verification or retention-binding mismatch. ## Recover the database `POST /api/v1/databases/{id}/recover` When an older deletion has no recorded connection cap and native database caps are enabled, the endpoint returns `409 RECOVERY_CAP_SELECTION_REQUIRED` with the currently allowed range. Retry with the same required `Idempotency-Key` and `{ "serverConnectionCap": 2 }` (choose a value within that range). This records a new customer choice for the recovered database; it does not claim to reconstruct the previous cap. A supplied cap cannot override a saved cap or an invalid or conflicting identity/OID binding. The CLI accepts the same choice with `--connection-cap N`. If native caps are explicitly disabled, recovery keeps the current uncapped server policy; that is not evidence of the database's previous cap. Unknown native mode or conflicting identity/OID evidence stops recovery. Returns `202 Accepted` with an operation. Recovery requires available plan quota and restores the isolated PostgreSQL generation, active team memberships, the IP allowlist and still-existing selected-database API-key bindings. Missing users or keys are skipped and reported in the completed operation. Pending invitations are not restored. A deleted preview uses this same endpoint and a full-account key; a CI preview key cannot recover a deleted database. A successful recovery starts a new 24-hour preview lease. Later lease renewals can request up to 72 hours. Repeated requests reuse the recovery operation for that deletion. Refusals return `409` with a code: `RECOVERY_CAPACITY_UNAVAILABLE` (your current or pending plan has no free database slot; a recovery that already completed never holds a slot again, even after the database is deleted a second time), `RECOVERY_WINDOW_CLOSED`, `RECOVERY_BACKUP_UNAVAILABLE`, `RECOVERY_NOT_ENABLED`, `RECOVERY_OPERATION_TERMINAL`, `RECOVERY_CAP_SELECTION_INVALID`, `RECOVERY_CAP_SELECTION_NOT_ALLOWED`, `RECOVERY_CAP_SELECTION_REPLAY_CONFLICT` or `RECOVERY_SETTINGS_RECONCILIATION_REQUIRED`. Other refusals use `RECOVERY_NOT_AVAILABLE`. After `purgeAfter`, download and recovery are disabled and DBHost permanently deletes the database and its deletion backup automatically. Permanent deletion can be briefly delayed by a safe retry; both are removed before the item disappears from this list. # Set database environment label Source: https://docs.dbhost.app/api-reference/databases/environment PATCH /api/v1/databases/{id}/environment Change a DBHost database's environment label to production, staging, development, other or none through the API. Backup schedule is unchanged. Sets the environment label of one database. Selected-database keys can change databases they are assigned to; full-account keys can change any database you own. API access requires a Pro or Business plan. > [!NOTE] > > This route changes the label only. It does not change the database's > scheduled backup choice, which is set when the database is created. See > [Backups and retention](/backups-and-retention) for the backup policy per > environment. ## Request body | Field | Type | Required | Description | | - | - | - | - | | `environment` | string or `null` | Yes | `production`, `staging`, `development`, `other`, or `null` to clear the label | ```http theme={null} PATCH /api/v1/databases/{id}/environment Authorization: Bearer Content-Type: application/json {"environment":"staging"} ``` ## Response ```json 200 OK theme={null} { "message": "Database environment updated.", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "environment": "staging" } } ``` The change is recorded in the database's activity log. ## Errors | Status | Code | Description | | - | - | - | | 400 | `VALIDATION_ERROR` | `environment` is missing or not one of the allowed values | | 400 | `INVALID_JSON` | Request body is not valid JSON | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 403 | `PLAN_API_ACCESS_REQUIRED` | The account is not on Pro or Business | | 404 | `DATABASE_NOT_FOUND` | Database does not exist or is not visible to this key | | 413 | `PAYLOAD_TOO_LARGE` | Request body is larger than 64 KB | | 429 | `RATE_LIMITED` | Too many requests; retry after `Retry-After` seconds | The CLI equivalent is `dbhost databases environment `. In the dashboard, see [Databases](/dashboard/databases). # Extensions API: list, install and upgrade Source: https://docs.dbhost.app/api-reference/databases/extensions Read the citext, pg_trgm and pgcrypto catalog for a DBHost database, then install or upgrade one extension as an idempotent operation. DBHost supports three PostgreSQL 16 extensions: `citext`, `pg_trgm` and `pgcrypto`. These routes read the catalog for one database and install or upgrade one extension at a time. For what each extension is for, see [PostgreSQL extensions](/guides/postgres-extensions). * API access requires a Pro or Business plan. See [Authentication](/api-reference/authentication). * A selected-database key works for the databases it is assigned to. A full-account key works for every database you own. * Install and upgrade work only on databases with [managed SQL roles](/guides/database-roles). A single-login database returns the catalog but cannot install or upgrade. All three routes respond with `Cache-Control: no-store`. ## List extensions `GET /api/v1/databases/{id}/extensions` The database must be active. The response always lists the three extensions in the order `citext`, `pg_trgm`, `pgcrypto`. ```http theme={null} GET /api/v1/databases/{id}/extensions Authorization: Bearer ``` ```json 200 OK theme={null} { "version": 1, "databaseId": "550e8400-e29b-41d4-a716-446655440000", "databaseName": "my-app", "databaseOid": 16412, "postgresVersionNum": 160009, "extensions": [ { "name": "citext", "available": true, "defaultVersion": "1.6", "installedVersion": "1.6", "installedSchema": "public", "trusted": true, "catalogProfile": "sql_verified", "restoreQualified": true }, { "name": "pg_trgm", "available": true, "defaultVersion": "1.6", "installedVersion": null, "installedSchema": null, "trusted": true, "catalogProfile": "sql_verified", "restoreQualified": true }, { "name": "pgcrypto", "available": true, "defaultVersion": "1.3", "installedVersion": null, "installedSchema": null, "trusted": true, "catalogProfile": "sql_verified", "restoreQualified": true } ] } ``` | Field | Description | | - | - | | `databaseOid` | PostgreSQL object ID of the database | | `postgresVersionNum` | PostgreSQL server version as a number, for example `160009` for 16.9 | | `available` | The extension is present on the server. When `false`, the version, schema and `trusted` fields are `null`. | | `defaultVersion` | Version the server would install | | `installedVersion` | Version installed in this database, or `null` | | `installedSchema` | Schema the extension is installed in, or `null` | | `trusted` | Whether PostgreSQL marks the extension as trusted, or `null` when not available | | `catalogProfile` | `sql_verified` when DBHost has verified this version on the server, otherwise `candidate` | | `restoreQualified` | `true` when DBHost can install or upgrade this extension and keep it through a restore | DBHost installs `citext` 1.6, `pg_trgm` 1.6 and `pgcrypto` 1.3 in the `public` schema. ## Install or upgrade an extension `POST /api/v1/databases/{id}/extensions/operations` Requests one install or upgrade. The change runs as an asynchronous operation. ```http theme={null} POST /api/v1/databases/{id}/extensions/operations Authorization: Bearer Idempotency-Key: install-citext-0001 Content-Type: application/json {"extension":"citext","kind":"install"} ``` ### Headers | Header | Required | Rule | | - | - | - | | `Idempotency-Key` | Yes | 8 to 128 characters from `A-Z`, `a-z`, `0-9`, `.`, `_`, `:` and `-` | Reuse the same key after a timeout or an uncertain response. The same key returns the same operation and never starts a second change. A new key requests a new operation. Reusing a key with a different extension or kind is rejected with `409 EXTENSION_MUTATION_UNAVAILABLE`. ### Request body The body must contain exactly these two fields and nothing else. | Field | Type | Values | | - | - | - | | `extension` | string | `citext`, `pg_trgm` or `pgcrypto` | | `kind` | string | `install` or `upgrade` | * `install` requires that the extension is not installed in the database. * `upgrade` requires that the extension is installed in the `public` schema at a version other than the supported version. * The catalog entry must be `available`, `trusted` and `restoreQualified`, and its `defaultVersion` must be the supported version. Requests that do not meet these conditions, or that arrive while a restore is running on the database, return `409 EXTENSION_MUTATION_UNAVAILABLE`. ### Response `202 Accepted` while the operation is `queued` or `running`, or `200 OK` if it has already reached a terminal status (for example on a replay). The `Location` header points to the operation status route. ```json 202 Accepted theme={null} { "operation": { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "type": "extension", "status": "queued", "stage": "queued", "progress": 0, "databaseId": "550e8400-e29b-41d4-a716-446655440000", "databaseName": "my-app", "idempotencyKey": "", "message": null, "result": null, "error": null, "queuedAt": "2026-10-14T09:00:00.000Z", "startedAt": null, "completedAt": null, "updatedAt": "2026-10-14T09:00:00.000Z" } } ``` `idempotencyKey` is a value DBHost derives from your header. It is not the header value you sent. ## Get an extension operation `GET /api/v1/databases/{id}/extensions/operations/{operationId}` Returns the current state of one extension operation for this database. While the operation is open, each read also checks for progress, so poll this route until `status` is `succeeded`, `failed` or `canceled`. ```http theme={null} GET /api/v1/databases/{id}/extensions/operations/{operationId} Authorization: Bearer ``` ```json 200 OK theme={null} { "operation": { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "type": "extension", "status": "succeeded", "stage": "completed", "progress": 100, "databaseId": "550e8400-e29b-41d4-a716-446655440000", "databaseName": "my-app", "idempotencyKey": "", "message": "Extension change completed.", "result": { "extension": "citext", "installedVersion": "1.6", "schema": "public" }, "error": null, "queuedAt": "2026-10-14T09:00:00.000Z", "startedAt": "2026-10-14T09:00:00.000Z", "completedAt": "2026-10-14T09:00:06.000Z", "updatedAt": "2026-10-14T09:00:06.000Z" } } ``` `result` is set after success and contains `extension`, `installedVersion` and `schema`. A failure before any change to the database reports `error.code` `EXTENSION_PREPARATION_FAILED`. If the outcome cannot be confirmed yet, the operation stays `running` until DBHost confirms it; keep polling with the same operation ID rather than submitting a new request. `stage` values are informational and may change; base client logic on `status`. See [Get operation](/api-reference/operations/get) for operations in general. ## Errors | Status | Code | Description | | - | - | - | | 400 | `VALIDATION_ERROR` | Invalid database or operation ID, a body that is not exactly `extension` and `kind`, an unsupported value, or a missing or malformed `Idempotency-Key` | | 400 | `INVALID_JSON` | Request body is not valid JSON | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 403 | `PLAN_API_ACCESS_REQUIRED` | The account is not on Pro or Business | | 404 | `DATABASE_NOT_FOUND` | Database does not exist or is not visible to this key | | 404 | `OPERATION_NOT_FOUND` | No extension operation with this ID for this database | | 409 | `EXTENSION_MUTATION_UNAVAILABLE` | The database or extension does not meet the install or upgrade conditions, a restore is running, or the idempotency key belongs to a different request | | 413 | `PAYLOAD_TOO_LARGE` | Request body is larger than 64 KB | | 429 | `RATE_LIMITED` | Too many requests; retry after `Retry-After` seconds | | 503 | `EXTENSION_CATALOG_UNAVAILABLE` | The catalog could not be read, for example because the database is not active | | 503 | `EXTENSION_MUTATION_UNAVAILABLE` | The change could not be requested right now; retry with the same key | | 503 | `MAINTENANCE` | Scheduled maintenance; retry after `Retry-After` seconds | During maintenance the body is `{"error":"MAINTENANCE","message":"..."}` instead of the standard error envelope. The dashboard offers the same actions on the database page; see [Databases](/dashboard/databases). The CLI equivalents are `dbhost databases extensions `, `dbhost databases extensions install|upgrade --idempotency-key KEY` and `dbhost databases extensions status `. # Get database Source: https://docs.dbhost.app/api-reference/databases/get GET /api/v1/databases/{id} Get one DBHost database by ID through the API, including status, size, connection details and its effective scheduled backup policy. Returns full details for a database. `sizeBytes` reflects the latest known PostgreSQL database size. When the agent is reachable, DBHost refreshes this from live agent data before responding and also persists the refreshed value for later reads. `status` can be `creating`, `active`, `stopped`, `deleting`, or `error`. `backupPolicy` reports the effective version, `scheduledBackup`, 30-day normal history, seven-day deletion recovery and whether the scheduler may enqueue a daily backup. A `legacy` profile keeps the existing database's prior daily behavior, even if its environment label changes. ## Path parameters | Parameter | Type | Description | | - | - | - | | `id` | uuid | Database ID | ## Example ```bash theme={null} curl -H "Authorization: Bearer dbh_your_api_key_here" \ https://dbhost.app/api/v1/databases/550e8400-e29b-41d4-a716-446655440000 ``` ## Response ```json 200 OK theme={null} { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "my-app", "displayName": "My Application", "host": "db.dbhost.app", "port": 6432, "database": "my-app", "username": "my-app", "status": "active", "sizeBytes": 8388608, "createdAt": "2026-03-19T12:00:00.000Z" } ``` ## Errors | Status | Code | Description | | - | - | - | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 404 | `DATABASE_NOT_FOUND` | Database doesn't exist, isn't owned by this user, or isn't in scope for this key | # Start, stop or restart a database Source: https://docs.dbhost.app/api-reference/databases/lifecycle POST /api/v1/databases/{id}/lifecycle Start, stop or restart a DBHost PostgreSQL database through the API. Stop blocks new connections and ends sessions; data is kept. Starts, stops or restarts one database. The request waits until the action has finished and returns the new status. Selected-database keys can use this route for databases they are assigned to; full-account keys for any database you own. API access requires a Pro or Business plan. | Action | Effect | Resulting `status` | | - | - | - | | `stop` | Blocks new connections and ends active sessions. Data stays on disk. | `stopped` | | `start` | Allows connections again and waits until pooled clients can connect | `active` | | `restart` | Ends active sessions, then allows connections again and waits until pooled clients can connect | `active` | Applications lose their open connections on `stop` and `restart`. Stopping a database does not delete it or its backups. ## Request body | Field | Type | Required | Description | | - | - | - | - | | `action` | string | Yes | `start`, `stop` or `restart` | ```http theme={null} POST /api/v1/databases/{id}/lifecycle Authorization: Bearer Content-Type: application/json {"action":"stop"} ``` ## Response ```json 200 OK theme={null} { "message": "Database stopped. New connections are blocked until it is started again.", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "status": "stopped" } } ``` The action is recorded in the database's activity log. ## Errors | Status | Code | Description | | - | - | - | | 400 | `VALIDATION_ERROR` | `action` is missing or not `start`, `stop` or `restart` | | 400 | `INVALID_JSON` | Request body is not valid JSON | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 403 | `PLAN_API_ACCESS_REQUIRED` | The account is not on Pro or Business | | 404 | `DATABASE_NOT_FOUND` | Database does not exist or is not visible to this key | | 409 | State code, for example `DATABASE_STOPPED` | The database's current state does not allow the action | | 413 | `PAYLOAD_TOO_LARGE` | Request body is larger than 64 KB | | 429 | `RATE_LIMITED` | Too many requests; retry after `Retry-After` seconds | | 502 | `AGENT_ERROR` | DBHost could not complete the action; check the status and retry | | 503 | `MAINTENANCE` | Scheduled maintenance; retry after `Retry-After` seconds | During maintenance the body is `{"error":"MAINTENANCE","message":"..."}` instead of the standard error envelope. The dashboard has the same controls in the database's operations menu; see [Databases](/dashboard/databases). The CLI equivalent is `dbhost databases start|stop|restart `. # List databases Source: https://docs.dbhost.app/api-reference/databases/list GET /api/v1/databases List the DBHost databases visible to your API key, newest first. Selected-database keys only see the databases assigned to them. Returns the databases visible to the authenticated API key, ordered by creation date (newest first). * Full-account keys return all owned databases. * Selected-database keys return only the databases assigned to that key. * `sizeBytes` reflects the latest known PostgreSQL database size. When the agent is reachable, DBHost refreshes this from live agent data before responding and also persists the refreshed value for later reads. * `status` can be `creating`, `active`, `stopped`, `deleting`, or `error`. ## Example ```bash theme={null} curl -H "Authorization: Bearer dbh_your_api_key_here" \ https://dbhost.app/api/v1/databases ``` ## Response ```json 200 OK theme={null} { "data": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "my-app", "displayName": "My Application", "status": "active", "sizeBytes": 8388608, "createdAt": "2026-03-19T12:00:00.000Z" } ], "pagination": { "page": 1, "limit": 20, "total": 1 } } ``` `data` is empty if the key has no visible databases. # Create a database with managed SQL roles Source: https://docs.dbhost.app/api-reference/databases/managed-create POST /api/v1/databases/managed Create a DBHost database with separate owner, runtime and read-only roles through the API, with optional reservation before provisioning. > [!NOTE] > > Fresh managed databases are opt-in from the dashboard on every plan. This > API endpoint is available on Pro and Business with a full-account API key. > The server chooses role names, connection budgets, and the database endpoint. > See [Database roles](/guides/database-roles) and > [Restore from a DBHost backup](/guides/restore-from-backup). Send a stable `Idempotency-Key` header with `name` and `displayName`. Ordinary creation returns `202` while pending and `201` when completed. Responses contain operation metadata, never passwords. Retrieve credentials separately. For a new standard database, optional `environment` is `production` (default), `staging`, `development` or `other`. Production requires daily scheduled logical backup. Staging defaults to daily and can explicitly choose `none`; development and other require `scheduledBackup: "none"` or `"daily"`. Both fields are bound to the idempotency key before SQL dispatch. Changing an existing database's environment label does not silently change its backup policy. ## Reserve before creating To obtain a stable target identity without dispatching database SQL or admitting connections, add `reserveOnly: true`: ```json theme={null} { "name": "example-staging", "displayName": "Example staging", "reserveOnly": true } ``` The `202` response contains `operationId`, `generation`, `status: "reserved"` and `targetDatabaseId`. The target database does not yet exist. Its reservation counts against the account's database quota, including pending reservations and any lower pending-plan limit. An unlimited admin plan retains its existing quota semantics. Repeat without `reserveOnly`, or with `reserveOnly: false`, using the same names and idempotency key to create and admit that exact identity. A completed replay returns completed metadata without changing the database. If SQL was already dispatched with an unresolved outcome, a reservation request returns `409` and does not reset or restart the operation. Do not send credentials, generated role names, budgets or endpoint settings. Unknown request fields and non-boolean `reserveOnly` values are rejected. All responses use `Cache-Control: no-store`. ## Clone reservation Any managed database you own can be cloned from one of its verified backups, with the **Clone** action on the dashboard's **Backups** tab or with `POST /api/v1/databases/{id}/managed-clones`. A database with legacy additional roles cannot be cloned (`409 LEGACY_OVERLAY_REQUIRES_V4`). The endpoint accepts the same `reserveOnly` boolean alongside `backupId`, `name` and `displayName`. A small clone completes in one request (`201`); repeat the request with the same idempotency key until it returns `201` with `status: "completed"`. Clone and managed-restore requests are limited to six per hour per API key, so do not poll faster than every 15 seconds. The source database, verified backup and future target identity are reserved together. A reserved response contains no credentials and dispatches no SQL or import. Continue using the same source, backup, target and idempotency key with `reserveOnly` false or omitted. Claimed or unknown SQL/import outcomes require reconciliation and cannot be converted back into reservations. When the database agent cannot create a clone target yet, a new request is refused before anything is reserved with `409 CLONE_NOT_AVAILABLE`. If the database agent refused a request before it created anything, a later call with the same key returns `409 MANAGED_DISPATCH_NO_EFFECT`: nothing was created, the reservation and its name are released, and you can start again with a new idempotency key. This happens no sooner than 15 minutes after the refusal. # Restore a managed-roles database from its backup Source: https://docs.dbhost.app/api-reference/databases/managed-restores Restore a DBHost database with managed SQL roles in place from one of its own verified backups, then advance the operation until it succeeds. Restores a database with managed SQL roles in place from a verified DBHost backup of that same database. The restore replaces the current contents, so writes made after the backup are no longer present. The role usernames and passwords stay the same. For other database types, see [Restore from a DBHost backup](/guides/restore-from-backup). Every database with managed roles can be restored, whether it has only its owner, runtime and read-only roles or also additional roles. The backup must list the same roles the database has now: after you add or remove an additional role, restore from a backup taken after that change. An older backup is refused when its roles no longer match: the restore operation then ends as `failed` with a message saying so, and the database is left unchanged. Start a new restore from a backup taken after the latest role change. Both endpoints require Pro or Business and a **full-account** API key. A key scoped to selected databases receives `403 KEY_SCOPE_DENIED`; see [Authentication](/api-reference/authentication). Every response, including errors, is sent with `Cache-Control: no-store`. ## How it works 1. Find a backup ID with `GET /api/v1/databases/{id}/backups`. 2. Reserve the restore with `POST /api/v1/databases/{id}/managed-restores`. This returns an operation ID and does not change the database yet. 3. Call `POST /api/v1/operations/{operationId}/managed-restore/advance` repeatedly until it returns `200` with `status: "succeeded"`. 4. Read progress at any time with [`GET /api/v1/operations/{operationId}`](/api-reference/operations/get). Expect open connections to drop during the cutover; your application must reconnect. While the restore is in progress, role rotations and revocations, creating or removing additional roles, and extension changes are refused. ## Reserve a restore ```http theme={null} POST /api/v1/databases/{id}/managed-restores Authorization: Bearer Idempotency-Key: restore-2026-10-14-orders Content-Type: application/json {"backupId":"6f1c2d3e-4b5a-4c6d-8e7f-9a0b1c2d3e4f"} ``` ### Path parameters | Parameter | Type | Description | | - | - | - | | `id` | uuid | Database ID | ### Headers | Header | Required | Description | | - | - | - | | `Idempotency-Key` | Yes | 1–128 characters from `A-Z`, `a-z`, `0-9`, `.`, `_`, `:` and `-` | Generate the key once and keep it. If a request times out or the result is uncertain, send the same request with the same key to get the original operation back. Do not send a new key to retry; that would reserve a second restore. A key belongs to one restore across your account: using it with a different database or backup returns `409 MANAGED_ROLE_CONFLICT`. ### Body The body must contain exactly one field: | Field | Type | Description | | - | - | - | | `backupId` | uuid | ID of a backup of this database, in lowercase UUID form | The backup must be a verified DBHost backup of this same database that includes its managed role manifest and is still within retention. You cannot restore one database's backup into another database. ### Response ```json 202 Accepted theme={null} { "operationId": "3f0c8a62-1d2b-4c5e-9f7a-0b1c2d3e4f5a", "status": "queued", "stage": "queued" } ``` The status code is `202` until the operation has succeeded, then `200`. If the key refers to a restore that failed or was canceled, the response is `409 RESTORE_OPERATION_TERMINAL`; start a new restore with a new key. The reservation is refused with `409 MANAGED_ROLE_CONFLICT` when the backup does not qualify, a role rotation or revocation on the database is unfinished, an additional role is still being created or removed, or another operation holds the database. ## Advance the restore ```http theme={null} POST /api/v1/operations/{operationId}/managed-restore/advance Authorization: Bearer ``` Send no body. Each call moves the operation forward from its current state and returns it: ```json 202 Accepted theme={null} { "operationId": "3f0c8a62-1d2b-4c5e-9f7a-0b1c2d3e4f5a", "status": "running", "stage": "managed_restore_source_verified" } ``` Repeat the call for the same operation until it returns `200` with `"status": "succeeded"`. Calling it again after success returns the same result. `stage` is informational and its values can change; rely on `status`. Do not reserve a second restore to retry a stage. DBHost verifies the backup and restores into an isolated copy before it switches over. If verification fails, the current database is left unchanged. ## Errors | Status | Code | Endpoint | Description | | - | - | - | - | | 400 | `VALIDATION_ERROR` | Reserve | Invalid database ID, body is not exactly `{"backupId": ""}`, or missing or invalid `Idempotency-Key` | | 400 | `INVALID_JSON` | Reserve | Request body is not valid JSON | | 401 | `UNAUTHORIZED` | Both | Missing or invalid API key | | 403 | `PLAN_API_ACCESS_REQUIRED` | Both | API access requires Pro or Business | | 403 | `KEY_SCOPE_DENIED` | Both | The key is scoped to selected databases; use a full-account key | | 404 | `OPERATION_NOT_FOUND` | Advance | No managed restore operation with this ID is visible to the account | | 409 | `RESTORE_SOURCE_REJECTED` | Advance | DBHost refused the selected backup (it no longer matches the database's roles, is missing, or failed verification). `error.message` says why; the database is unchanged and the operation is over. Choose another backup and start a new restore with a new key | | 409 | `RESTORE_OPERATION_TERMINAL` | Both | The restore failed or was canceled; start a new one with a new key. After a refused backup, `error.message` carries the same reason | | 409 | `MANAGED_ROLE_CONFLICT` | Both | The backup or database does not qualify, the key is bound to a different restore, or the operation cannot advance from its current state | | 413 | `PAYLOAD_TOO_LARGE` | Reserve | Request body is larger than the API limit | | 429 | `RATE_LIMITED` | Both | Too many requests; retry after the `Retry-After` header | | 503 | `FEATURE_DISABLED` | Both | Managed restore is temporarily unavailable; retry later | | 503 | `MAINTENANCE` | Both | DBHost is in a maintenance window; retry later | | 500 | `INTERNAL_ERROR` | Both | The request failed; retry with the same key or advance the same operation | The reserve endpoint allows six requests per hour per API key, including retries. The dashboard and the CLI have no restore action for databases with managed SQL roles. # Direct migration connection API Source: https://docs.dbhost.app/api-reference/databases/migration-connection Request, inspect and revoke short-lived direct access on migrate.dbhost.app:5432 for migration tools on a database with managed SQL roles. The direct migration channel is available for databases with [managed SQL roles](/guides/database-roles). API access requires Pro or Business. All routes require a scoped API key for that database. Responses have `Cache-Control: no-store`; metadata never includes a password. | Method | Path | Result | | - | - | - | | `GET` | `/api/v1/databases/{id}/connection-profiles/migration` | Latest lease metadata, status, exact IPs, limits and endpoint when active. | | `POST` | Same path | Request a lease; send a stable `Idempotency-Key`. Returns 202 until DBHost has opened access. | | `POST` | `.../migration/credentials` | Return the password to an owner-scoped, mutating API key only while the exact lease is active and unexpired. | | `DELETE` | Base path | Cancel an unused reservation or disable the SQL login and request network-rule cleanup. | `POST` accepts only `allowedAddresses` (one or two exact public IPv4/IPv6 addresses), `ttlMinutes` (10–120), `connectionLimit` (1 or 2), `statementTimeoutMs` (60000–1800000) and `lockTimeoutMs` (1000–30000, less than the statement timeout). The server canonicalizes the IPs and verifies the current database, role set and owner before it stores the request. The dashboard offers leases of 30, 60 or 120 minutes. A request is reserved first; it is not a working connection yet. DBHost then checks the database, the TLS certificate and the session budget, opens the network path for your exact IPs only and enables the login. `GET` returns the latest lease as `{ "lease": { … } }`, or `{ "lease": null }` when the database has none, and reports where the lease is: | Field | Values | | - | - | | `activation` | `automatic` when DBHost opens access itself, usually within a minute; `operator` for a lease that waits for a DBHost operator. | | `stage` | `queued` (waiting for the host, for example while another direct migration closes), `preparing`, `opening_network`, `active`, `revoking`, `revoked`, `failed`, `waiting_for_operator` or `needs_reconciliation`. | | `failureReason` | Why a `failed` request did not open, in plain words; otherwise `null`. | The lease's time window starts when the login is prepared, not when you request it, so waiting never shortens it. A request that cannot open safely is closed with `stage: "failed"`; request again with a new `Idempotency-Key`. Only after `stage` is `active` does the role log in. The active endpoint uses `migrate.dbhost.app:5432` with `sslmode=verify-full`. The runtime connection continues to use PgBouncer. `DELETE` can return `revoking` while DBHost drops the login and removes that lease's network rules; it then becomes `revoked` without further action. A new request waits until this cleanup is finished. A 404 `FEATURE_NOT_AVAILABLE` means the channel is not available for this database, for example because it has no managed SQL roles; a 409 means the database or network state must be reconciled before access opens. CLI 0.3.7 or newer provides `dbhost databases migration`, `migration-request`, `migration-credentials` and `migration-revoke`. For tool setup and expiry behavior, see [Direct database migrations](/guides/direct-migrations). # IP allowlist API: list, add and remove CIDRs Source: https://docs.dbhost.app/api-reference/databases/network-allowlist Manage which client networks can connect to a DBHost database through PgBouncer. An empty allowlist accepts connections from any IP. The IP allowlist controls which client networks can connect to a database through PgBouncer on port `6432`. It does not affect dashboard or API access. * An empty allowlist accepts connections from any IP address. * As soon as the list has one or more entries, only matching networks can connect. * Removing the last entry opens the database to any IP again. Selected-database keys can use these routes for databases they are assigned to; full-account keys for any database you own. API access requires a Pro or Business plan. ## List entries `GET /api/v1/databases/{id}/network/allowlist` ```http theme={null} GET /api/v1/databases/{id}/network/allowlist Authorization: Bearer ``` ```json 200 OK theme={null} { "data": [ { "id": "3f2b8c1e-9a4d-4b7e-8f21-6c0d5e7a9b10", "cidr": "198.51.100.0/24", "label": "Office", "createdAt": "2026-10-14T09:00:00.000Z" } ] } ``` ## Add an entry `POST /api/v1/databases/{id}/network/allowlist` | Field | Type | Required | Description | | - | - | - | - | | `cidr` | string | Yes | IPv4 or IPv6 block in `address/prefix` form, for example `198.51.100.0/24` or `2001:db8::/48`. Use `/32` (IPv4) or `/128` (IPv6) for a single address. | | `label` | string | No | Note for the entry, at most 120 characters after trimming. Defaults to an empty string. | The prefix is required; a bare IP address is rejected. DBHost stores the CIDR as you send it, after trimming whitespace. ```http theme={null} POST /api/v1/databases/{id}/network/allowlist Authorization: Bearer Content-Type: application/json {"cidr":"198.51.100.0/24","label":"Office"} ``` ```json 201 Created theme={null} { "message": "Allowlist entry added.", "data": { "id": "3f2b8c1e-9a4d-4b7e-8f21-6c0d5e7a9b10", "databaseId": "550e8400-e29b-41d4-a716-446655440000", "cidr": "198.51.100.0/24", "label": "Office", "createdByUserId": "", "createdAt": "2026-10-14T09:00:00.000Z" } } ``` Adding a CIDR that is already on the list returns `200 OK` with the message `Allowlist entry already exists.` and the existing entry. The existing label is not changed. The new list is applied before the request returns. If DBHost cannot apply it, the request fails and the entry is not saved. ## Remove an entry `DELETE /api/v1/databases/{id}/network/allowlist/{entryId}` `entryId` is the `id` from the list or add response. On success the route returns `204 No Content` with an empty body. ```http theme={null} DELETE /api/v1/databases/{id}/network/allowlist/{entryId} Authorization: Bearer ``` ## Errors | Status | Code | Description | | - | - | - | | 400 | `VALIDATION_ERROR` | `cidr` is not a valid IPv4 or IPv6 CIDR block, or `label` is longer than 120 characters | | 400 | `INVALID_JSON` | Request body is not valid JSON | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 403 | `PLAN_API_ACCESS_REQUIRED` | The account is not on Pro or Business | | 404 | `DATABASE_NOT_FOUND` | Database does not exist or is not visible to this key | | 404 | `ALLOWLIST_ENTRY_NOT_FOUND` | No entry with this ID on this database | | 409 | State code, for example `DATABASE_STOPPED` | The database's current state does not allow the change | | 413 | `PAYLOAD_TOO_LARGE` | Request body is larger than 64 KB | | 429 | `RATE_LIMITED` | Too many requests; retry after `Retry-After` seconds | | 502 | `AGENT_ERROR` | The change could not be applied, for example while the database is being deleted. The list is unchanged. | | 503 | `MAINTENANCE` | Scheduled maintenance; retry after `Retry-After` seconds | During maintenance the body is `{"error":"MAINTENANCE","message":"..."}` instead of the standard error envelope. The dashboard manages the same list in the **Allowed IPs** section on the **Access** tab; see [Databases](/dashboard/databases). The CLI equivalents are `dbhost network list|add|remove`. # Query timeout defaults API Source: https://docs.dbhost.app/api-reference/databases/query-timeouts Read and select a DBHost query timeout profile through the API: standard 60/10/30 s, balanced 30/3/15 s or short 15/1/5 s per session. `GET /api/v1/databases/{id}/query-timeouts` returns the exact native database OID, current catalog-backed timeout values, the desired and effective profile, operation status, generation, and policy version. `not_configured` means DBHost has not selected a database profile; it does not imply unlimited queries for every role. The response reports when some roles have their own overrides. If native refuses the strict read before returning a profile, this endpoint returns `503 QUERY_TIMEOUT_NATIVE_READBACK_REFUSED`. A validated native response with defaults outside the supported profiles instead returns `503 QUERY_TIMEOUT_NATIVE_PROFILE_UNSUPPORTED`. Neither response claims the saved profile is active. The dashboard can still show the separate new-session role defaults from a database-bound catalog read. Neither read changes customer role overrides or silently rebinds the saved policy. | Profile | Statement | Lock wait | Idle in transaction | | - | -: | -: | -: | | `standard` | 60 s | 10 s | 30 s | | `balanced` | 30 s | 3 s | 15 s | | `short` | 15 s | 1 s | 5 s | An account owner can select one supported profile with an API key authorized to change that database: ```http theme={null} POST /api/v1/databases/{id}/query-timeouts Authorization: Bearer Idempotency-Key: Content-Type: application/json {"profile":"balanced"} ``` Reuse the same idempotency key after a timeout or uncertain response. An open change blocks a second request and database lifecycle work until it is reconciled. The profile sets database defaults and the owner login's database-specific defaults. PgBouncer reconnects server sessions after their current transactions; already running transactions are not interrupted. Customer-created role-specific overrides are preserved and may produce a different effective setting for those roles. These defaults are not hard CPU, RAM or disk quotas. The clone route returns `409 QUERY_TIMEOUT_CLONE_REQUIRES_RECONFIGURATION` for a database with a saved timeout profile. A clone needs independent admission and a verified target profile; DBHost does not silently copy the source profile. Same-identity restore keeps the profile after native readback and rebinds its physical database OID. The dashboard has the same profile selector with one confirmation. The CLI uses `dbhost databases timeouts ` and `dbhost databases timeouts-set --idempotency-key KEY`. # Repair ownership Source: https://docs.dbhost.app/api-reference/databases/repair-ownership POST /api/v1/databases/{id}/repair-ownership Repair object ownership for supported legacy imports. Managed databases with protected roles use their grants manifest instead. Repairs object ownership for a supported legacy database after a self-managed import. The agent refuses this operation on a managed database with a protected SQL Explorer role; that database requires the managed grants manifest path. ## When to use this Call this endpoint after you've imported a dump into a supported legacy database yourself (see [Restore your own dump](/guides/restore-your-own-dump)) and verified that an ownership mismatch causes the failure. **Why it happens:** A dump can carry object owners from the source server that differ from your DBHost owner role. Restores that DBHost runs from its own backups already end with this repair step. Use this endpoint for: * Clean-up after your own `psql` / `pg_restore` import. * Databases restored before the automatic repair existed. * A legacy import whose object owners differ from the database owner. If a managed Runtime or Readonly role receives `permission denied`, check its explicit `GRANT` and default privileges in your migrations. Changing the owner role does not grant access to those app roles. DBHost managed restores use a separate manifest-backed repair path. ## Path parameters | Parameter | Type | Description | | - | - | - | | `id` | uuid | Database ID | ## What it does For a supported legacy target database only: 1. `ALTER SCHEMA public OWNER TO ` + grants `USAGE, CREATE`. 2. Iterates every non-system schema and issues `ALTER TABLE/SEQUENCE/VIEW/MATERIALIZED VIEW ... OWNER TO `. 3. Restores read access for the dashboard SQL Explorer on the affected tables. The operation is scoped to a single database. It cannot affect any other tenant's data. ## Response ```json 200 OK theme={null} { "name": "mydb", "status": "repaired", "message": "Ownership repair completed for 'mydb'" } ``` ## Errors | Status | Code | Description | | - | - | - | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 404 | `DATABASE_NOT_FOUND` | Database doesn't exist, isn't owned by this user, or isn't in scope for this key | | 409 | `OPERATION_BINDING_CONFLICT` | Protected managed ownership requires the grants manifest path | | 500 | `AGENT_ERROR` | DBHost couldn't connect to the database or repair ownership | ## CLI ```bash theme={null} dbhost databases repair-ownership ``` > [!TIP] > > Running this when ownership is already correct is a no-op — every `ALTER ... OWNER TO` statement is idempotent. # Reset password Source: https://docs.dbhost.app/api-reference/databases/reset-password POST /api/v1/databases/{id}/reset-password Generate a new password for a single-login DBHost database through the API. The previous password stops working immediately. Generates a new password for the database. The old password stops working immediately. PgBouncer is reloaded automatically. The database must be active. Otherwise the request is refused before anything changes. IP allowlist rules are allowed once DBHost can verify the new password from its own host; until then a reset on a database with rules is refused with `IP_ALLOWLIST_BLOCKS_RESET`. A few databases still need their owner login for that check, such as databases with the public explorer enabled and older single-login databases. With allowlist rules they answer `DATABASE_NOT_READY`: remove the rules, reset, then add them back. ## Headers | Header | Required | Description | | - | - | - | | `Idempotency-Key` | No | Stable key for this reset. Resending the same key after an uncertain result returns the same outcome instead of starting another reset. The CLI always sends one and prints it. | ## Path parameters | Parameter | Type | Description | | - | - | - | | `id` | uuid | Database ID | ## Response ```json 200 OK theme={null} { "idempotencyKey": "your-stable-key", "operationId": null, "password": "new-generated-password" } ``` `idempotencyKey` is echoed only when you sent one. ## Errors | Status | Code | Description | | - | - | - | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 404 | `DATABASE_NOT_FOUND` | Database doesn't exist, isn't owned by this user, or isn't in scope for this key | | 409 | `DATABASE_STOPPED` | The database is stopped. Start it, then reset. Nothing was changed | | 409 | `DATABASE_PAUSED_INACTIVE` | DBHost paused the database for inactivity. Start it, then reset. Nothing was changed | | 409 | `DATABASE_NOT_ACTIVE` | The database is still being created, deleted or is in an error state. Nothing was changed | | 409 | `IP_ALLOWLIST_BLOCKS_RESET` | IP allowlist rules prevent DBHost from verifying the new password. Remove the rules, reset, then add them back. Nothing was changed | | 409 | `DATABASE_NOT_READY` | DBHost could not confirm pooled connections to the database. With IP allowlist rules, remove them, reset, then add them back; otherwise retry in a minute | | 409 | `ROTATION_IN_PROGRESS` | Another reset is still running. DBHost first asks its VPS agent what became of it: a finished or failed reset is settled and your request proceeds, and a reset the agent is still working on answers with the seconds it needs in `Retry-After`. If the agent cannot say, the header says when a new reset may replace an unfinished one, or you can end it with `DELETE` below | | 500 | `AGENT_ERROR` | The VPS agent failed to reset the password. Retry the reset | ## End an unfinished reset ``` DELETE /api/v1/databases/{id}/reset-password ``` Ends a reset that never finished so the next reset can start. DBHost reads the agent's record of the reset first. ```json 200 OK theme={null} { "status": "released" } ``` | `status` | Meaning | | - | - | | `none` | No unfinished reset was found | | `completed` | The earlier reset had in fact finished. Reset again to get a new password | | `released` | The unfinished reset was ended. Reset again | If the agent is still working on the reset, the answer is a `409 ROTATION_IN_PROGRESS` with the time it needs in `Retry-After`. When the agent cannot answer, a reset is ended once it is at least 60 seconds old. > [!WARNING] > > All active connections using the old password will fail. Update your application's connection string before or immediately after resetting. # Rotate or revoke a managed database role Source: https://docs.dbhost.app/api-reference/databases/role-changes POST /api/v1/databases/{id}/role-changes API for rotating or revoking the owner, runtime or read-only login of a database with managed SQL roles without touching the other roles. The API key must belong to the current database owner and include the database in its scope. Public profiles are `owner` (including migration access), `runtime` and `readonly`. Internal Explorer access is not a public profile. > [!WARNING] > > Rotation and revocation close existing connections for the selected role. Open > transactions on those connections roll back. Confirm this effect before sending > the request. Other role profiles remain available. ## Create an operation Send a stable `Idempotency-Key` header and this exact JSON body: ```json theme={null} { "profile": "runtime", "kind": "rotate", "confirmDisconnect": true } ``` Use `kind: "revoke"` to disable that login. Revoke preserves grants and data; rotation does not reactivate a revoked profile. Do not send a password, physical role name, database name, connection string or internal Explorer profile. The response is `202` while pending or `201` when completed, with a `Location` header pointing to the operation. It contains no credentials: ```json theme={null} { "operationId": "22222222-2222-4222-8222-222222222222", "status": "pending", "profile": "runtime", "kind": "rotate", "credentialGeneration": 2 } ``` Retry the same intent with the same key. A different profile or kind on that key is refused. An unknown result remains pending; it does not select another password or undo a change. Explicit credential retrieval is unavailable for the selected profile until completion. The separate credentials endpoint returns the current credential after a completed rotation. ## Read or reconcile `GET /api/v1/databases/{id}/role-changes/{operationId}` returns `200` with operation metadata. It performs no native mutation or reconciliation. To reconcile an uncertain outcome, send `POST` to that same operation URL with exactly `{"confirmDisconnect": true}`. The server reuses the recorded input and returns `202` pending or `200` completed. An already completed replay performs no native operation. Completed remote work can be finalized while new mutations are disabled; unfinished native work waits for mutation availability and an active database. Stopped databases retain metadata and completed replay; they cannot reserve a new role change or dispatch a native credential mutation. ## Errors | Status | Code | Meaning | | - | - | - | | 400 | `VALIDATION_ERROR` | Invalid profile/kind, missing acknowledgement or idempotency key | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 404 | `DATABASE_NOT_FOUND` | Database is not accessible to this key | | 409 | `MANAGED_ROLE_CONFLICT` | Binding/intent changed, or operator reconciliation is required | | 503 | `FEATURE_UNAVAILABLE` | Role changes are temporarily unavailable for this database; retry later | | 500 | `INTERNAL_ERROR` | Request failed; retry the same intent or inspect its existing operation | All responses use `Cache-Control: no-store`. An unresolved operation prevents owner reassignment until verified completion. # Managed roles API: list roles and reveal a credential Source: https://docs.dbhost.app/api-reference/databases/roles List the owner, runtime and read-only logins of a DBHost database with managed SQL roles, and reveal one role's password through the API. A database with managed SQL roles has three base logins: `owner` for migrations, `runtime` for your application and `readonly` for reporting. These endpoints list them and reveal one password at a time. To rotate or revoke a base role, use [Managed role changes](/api-reference/databases/role-changes). For additional roles, see [Additional roles on managed databases](/api-reference/databases/custom-roles). See [Database roles](/guides/database-roles) for the concepts. API access requires Pro or Business. Both endpoints accept a full-account key or a key scoped to selected databases that includes this database; see [Authentication](/api-reference/authentication). Every response, including errors, is sent with `Cache-Control: no-store`. ## List base roles ```http theme={null} GET /api/v1/databases/{id}/roles Authorization: Bearer ``` Returns metadata only; no password is included. ```json 200 OK theme={null} { "managed": true, "roles": [ { "profile": "owner", "username": "dbr_550e8400e29b41d4a716446655440000_own", "connectionLimit": 2, "generation": 1, "credentialGeneration": 1, "status": "ready", "operationId": null, "lastRotatedAt": null }, { "profile": "runtime", "username": "dbr_550e8400e29b41d4a716446655440000_run", "connectionLimit": 4, "generation": 1, "credentialGeneration": 2, "status": "ready", "operationId": null, "lastRotatedAt": "2026-10-01T09:12:44.000Z" }, { "profile": "readonly", "username": "dbr_550e8400e29b41d4a716446655440000_read", "connectionLimit": 2, "generation": 1, "credentialGeneration": 1, "status": "ready", "operationId": null, "lastRotatedAt": null } ] } ``` A database without managed SQL roles returns `{"managed": false, "roles": []}`. Roles are always ordered `owner`, `runtime`, `readonly`. | Field | Description | | - | - | | `profile` | `owner`, `runtime` or `readonly` | | `username` | Generated PostgreSQL login name. The database name is not a username | | `connectionLimit` | The role's own PostgreSQL connection limit | | `generation` | Role-set generation for the database | | `credentialGeneration` | Increases by one each time the password changes | | `status` | `ready`, `revoked`, `updating` or `expired` | | `operationId` | ID of the pending role change while `status` is `updating`, otherwise `null` | | `lastRotatedAt` | Time of the most recent completed rotation, or `null` | `updating` means a rotation or revocation for that role has not finished; read it with `GET /api/v1/databases/{id}/role-changes/{operationId}`. `expired` means the credential's validity period has ended and it can no longer be revealed. ## Reveal a credential ```http theme={null} POST /api/v1/databases/{id}/roles/credentials Authorization: Bearer Content-Type: application/json {"profile":"runtime"} ``` The body must contain exactly one field, `profile`, set to `owner`, `runtime` or `readonly`. The role must be `ready`. Each reveal is recorded in the account's audit log. ```json 200 OK theme={null} { "profile": "runtime", "username": "dbr_550e8400e29b41d4a716446655440000_run", "connectionLimit": 4, "generation": 1, "credentialGeneration": 2, "status": "ready", "host": "db.dbhost.app", "port": 6432, "database": "my-app", "password": "", "tlsRequired": true } ``` Connect with TLS; `tlsRequired` is always `true`. Store the password in a secret manager and keep it out of logs and source control. A role with a pending change, a revoked role and an expired role return `409 MANAGED_ROLE_CONFLICT`. ## Errors | Status | Code | Description | | - | - | - | | 400 | `VALIDATION_ERROR` | Reveal body is not exactly one `profile` field set to `owner`, `runtime` or `readonly` | | 400 | `INVALID_JSON` | Request body is not valid JSON | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 403 | `PLAN_API_ACCESS_REQUIRED` | API access requires Pro or Business | | 404 | `DATABASE_NOT_FOUND` | Database doesn't exist, isn't owned by this account or isn't in the key's scope | | 409 | `MANAGED_ROLE_CONFLICT` | The role is not available in its current state, for example revoked, expired or changing | | 413 | `PAYLOAD_TOO_LARGE` | Request body is larger than the API limit | | 429 | `RATE_LIMITED` | Too many requests; retry after the `Retry-After` header | | 503 | `FEATURE_DISABLED` | Managed roles are temporarily unavailable | | 503 | `MAINTENANCE` | DBHost is in a maintenance window; retry later | | 500 | `INTERNAL_ERROR` | The request failed; retry | ## Dashboard The database detail page shows the same roles and lets you reveal, rotate or revoke one credential at a time on every plan. # DBHost REST API introduction Source: https://docs.dbhost.app/api-reference/introduction Use the DBHost REST API on Pro and Business to create databases, run backups and restores, manage roles and automate CI with scoped API keys. The DBHost REST API (`https://dbhost.app/api/v1/`) is authenticated with API keys. Use it from your applications, scripts, and CI/CD pipelines. Paid users can issue either selected-database keys or full-account keys. ## API key scope model Paid users can create two API key scope types: * **Selected databases** — Recommended least-privilege option. The key only sees and operates on its assigned databases. * **Full account access** — Can create and manage every database in the owning account. Selected-database keys only list their assigned databases. For database-specific routes, out-of-scope access behaves like a missing resource and returns `DATABASE_NOT_FOUND`. Full-account-only routes, such as database creation, return `KEY_SCOPE_DENIED` when used with a selected-database key. ## Base URL ``` https://dbhost.app/api/v1 ``` ## Common cURL examples List databases visible to the authenticated key: ```bash theme={null} curl -H "Authorization: Bearer dbh_your_api_key_here" \ https://dbhost.app/api/v1/databases ``` Create a database with a full-account key: ```bash theme={null} curl -X POST https://dbhost.app/api/v1/databases \ -H "Authorization: Bearer dbh_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{"name":"analytics-v2","displayName":"Analytics V2"}' ``` Trigger a backup: ```bash theme={null} curl -X POST \ -H "Authorization: Bearer dbh_your_api_key_here" \ https://dbhost.app/api/v1/databases//backups ``` ## Response format Most successful responses return JSON. File downloads such as backup exports return the raw attachment body, and some delete routes return `204 No Content`. Asynchronous v1 operations return `202 Accepted`, preserve a human-readable `message`, and include an `operation` object. Read status with `GET /api/v1/operations/{id}`. Operation types are `backup`, `restore`, `delete`, `recover`, and `clone`. Statuses are `queued`, `running`, `succeeded`, `failed`, and `canceled`. Errors use a standard envelope: ```json theme={null} { "error": { "code": "DATABASE_NOT_FOUND", "message": "Database 'foo' does not exist", "details": [] } } ``` Capacity and pause codes you may see on create, start and connection-cap requests: | Status | Code | Meaning | | - | - | - | | 503 | `CLUSTER_CAPACITY_FULL` | Measured capacity on the shared host is temporarily full. Nothing was created; retry the same request later | | 503 | `CLUSTER_ADMISSION_UNAVAILABLE` | Capacity could not be verified. Nothing was created; retry later | | 409 | `DATABASE_PAUSED_INACTIVE` | The database was paused for inactivity. Start it with `POST /api/v1/databases/{id}/lifecycle` and `{"action":"start"}` | | 409 | `DATABASE_OPERATION_IN_PROGRESS` | Another operation (backup, restore, clone or delete) is still running on this database. Nothing was changed; retry when it has finished | | 409 | `CUSTOM_ROLE_LIMIT_REACHED`, `CUSTOM_ROLE_PENDING`, `CONNECTION_CAP_REQUIRED`, `ROLE_BUDGET_UNAVAILABLE` | An additional role could not be reserved; the message says what to change | ## Rate limits The v1 API applies route-aware limits after authentication and uses shared control-plane PostgreSQL buckets so limits remain consistent across serverless instances. A limited response uses `429 RATE_LIMITED` and includes `Retry-After` plus the applicable rate-limit headers. Clients should honor `Retry-After`, add jitter, and avoid retrying non-idempotent requests without an idempotency key. Requests are authenticated and rate-limited before their bodies are read. ## Resource overview Account, access and lifecycle: * `POST /api/v1/databases/{id}/lifecycle` * `PATCH /api/v1/databases/{id}/environment` * `GET|POST /api/v1/databases/{id}/network/allowlist` * `DELETE /api/v1/databases/{id}/network/allowlist/{entryId}` * `GET|POST /api/v1/databases/{id}/members` * `PATCH|DELETE /api/v1/databases/{id}/members/{membershipId}` * `GET /api/v1/databases/{id}/invitations` * `POST /api/v1/databases/{id}/invitations/{invitationId}/resend` * `DELETE /api/v1/databases/{id}/invitations/{invitationId}` * `GET /api/v1/operations/{id}` * `GET|POST /api/v1/webhooks` * `POST /api/v1/webhooks/{id}/rotate` * `DELETE /api/v1/webhooks/{id}` Deletion and recovery (see [Delete database](/api-reference/databases/delete) and [Deleted databases](/api-reference/databases/deleted)): * `DELETE /api/v1/databases/{id}` * `GET /api/v1/databases/deleted` * `GET /api/v1/databases/{id}/deletion-backup` * `POST /api/v1/databases/{id}/recover` A deleted database can be recovered for seven days. After that, DBHost permanently deletes it and its deletion backup automatically. Restore from a DBHost backup of the same database (see [Restore from a DBHost backup](/guides/restore-from-backup)): * `POST /api/v1/databases/{id}/backups/{filename}` for a single-login database * `POST /api/v1/databases/{id}/legacy-overlay-restores` for a single-login database with additional roles * `POST /api/v1/databases/{id}/managed-restores` for a database with managed SQL roles Clone: * `POST /api/v1/databases/{id}/clone` clones a single-login database. A database with additional roles is rejected with `409 LEGACY_OVERLAY_REQUIRES_V4`. Customer-uploaded SQL and dump restore endpoints are disabled and return `409 CUSTOM_RESTORE_UPLOAD_DISABLED` for an authorized database. Use the database owner role with `psql` or `pg_restore` over TLS instead; see [Restore your own dump](/guides/restore-your-own-dump). Database creation, collaborator/invitation management, and customer webhooks require a full-account API key. A selected-database key can use database-specific lifecycle, environment, network, and backup routes only for its selected active databases. ## Extensions `GET /api/v1/databases/{id}/extensions` returns the supported extensions (`citext`, `pg_trgm`, `pgcrypto`) with the version available on the server and the version installed in the selected database. A selected-database API key can read only its assigned database. To install or upgrade one of them on a database with managed SQL roles, send `POST /api/v1/databases/{id}/extensions/operations` with an exact body `{"extension":"citext","kind":"install"}` (or `pg_trgm`, `pgcrypto` / `upgrade`) and a stable `Idempotency-Key` header. The response provides an operation ID for `GET /api/v1/databases/{id}/extensions/operations/{operationId}`. Check a pending or uncertain outcome by that ID; sending a new key requests a different operation. See [PostgreSQL extensions](/guides/postgres-extensions). # Get operation Source: https://docs.dbhost.app/api-reference/operations/get GET /api/v1/operations/{id} Read the status of an asynchronous DBHost operation such as a backup, restore, delete or recover by its operation ID through the API. Returns one operation visible to the authenticated API key. Operations cover `backup`, `restore`, `clone`, `delete`, and `recover` jobs. ## Path parameters | Parameter | Type | Description | | - | - | - | | `id` | string | Operation ID returned by an asynchronous route | ## Response ```json 200 OK theme={null} { "operation": { "id": "op_01J...", "type": "restore", "status": "running", "stage": "validate_artifact", "progress": 20, "databaseId": "3f6c1e2a-7b4d-4c1e-9a2b-5d8e0f1a2b3c", "databaseName": "app_production", "idempotencyKey": "restore-2026-07-20-orders", "message": null, "result": null, "error": null, "queuedAt": "2026-07-20T12:00:00Z", "startedAt": "2026-07-20T12:00:01Z", "completedAt": null, "updatedAt": "2026-07-20T12:00:04Z" } } ``` Statuses are `queued`, `running`, `succeeded`, `failed`, and `canceled`. `result` holds public, type-specific fields once the operation finishes. `stage` is operation-specific. `progress` is an integer from 0 to 100. Failed operations return a stable error code/message without exposing credentials or dump content. Clients can poll until a terminal status. The CLI waits by default; `--no-wait` returns the operation ID immediately. ## Errors | Status | Code | Description | | - | - | - | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 404 | `OPERATION_NOT_FOUND` | Operation is missing or outside the key's visible scope | # Preview CI lifecycle API Source: https://docs.dbhost.app/api-reference/previews/ci-lifecycle Create, inspect, renew, change backup choice for and delete preview databases from CI with a namespace-bound dbhci_ key on Pro and Business. > [!NOTE] > > Preview databases are available on Pro and Business. See > [Preview databases](/guides/preview-databases). Use a namespace-bound > `dbhci_` key for these endpoints. A full-account `dbh_` key cannot substitute > for a preview CI key. Previews are for synthetic test data; do not put > production or personal data in them. ## Create or replay `POST /api/v1/previews` accepts `ref`, optional `ttlHours` (1–72, default 24) and optional `scheduledBackup` (`none` or `daily`, default `none`). The server derives the internal name and idempotency key from the CI namespace and ref. Repeating the same ref and choices returns the same preview; changing the choices for that ref conflicts. Creation counts against the existing total database quota. Optionally add `"reserveOnly": true` to reserve capacity and receive `targetDatabaseId` without creating the physical database. Repeat the same ref, TTL and backup choice without `reserveOnly` after the target is available. The retry uses the same operation and does not reserve a second database. ```json theme={null} { "ref": "pull/42", "ttlHours": 24, "scheduledBackup": "none" } ``` No daily logical backup means there is no daily dump. Host snapshots may still contain the preview; they do not expire with the preview lease. DBHost keeps the three newest host snapshots, so this is not an age limit for a preview's data. See [Backups and retention](/backups-and-retention). Both backup choices require a verified deletion backup and preserve a seven-day recovery window when the preview is deleted. ## Inspect and renew `GET /api/v1/previews/{databaseId}` returns the database and namespace IDs, lease status and generation, expiry, backup choice and, after deletion, the recovery deadline. The response contains no credentials. `POST /api/v1/previews/{databaseId}/extend` accepts `{ "ttlHours": 24 }`. The value may be up to 72 hours per renewal. An expired or deleting lease cannot be renewed. To switch between `none` and `daily`, use `PATCH /api/v1/previews/{databaseId}/backup-policy` with `{ "scheduledBackup": "daily" }`. ## Delete and recover `DELETE /api/v1/previews/{databaseId}` takes no body. Deletion must finish with its own verified backup before the seven-day recovery window begins. Unknown agent outcomes remain held rather than creating a second deletion. The CI key cannot recover a deleted preview. An owner with a full-account key uses `POST /api/v1/databases/{databaseId}/recover` within seven days. A successful recovery starts a new 24-hour lease; later renewals can again be up to 72 hours. After seven days, the preview and its deletion backup are deleted permanently and automatically. # Webhooks API: operation events and signatures Source: https://docs.dbhost.app/api-reference/webhooks Register HTTPS endpoints for DBHost operation events, rotate signing secrets and verify the HMAC-SHA256 signature on each delivery. DBHost can send an HTTPS `POST` to your endpoint when an asynchronous operation, such as a backup or restore, reaches a final status. * These routes require a **full-account** API key. A selected-database key receives `403 KEY_SCOPE_DENIED`. See [API keys](/dashboard/api-keys). * API access requires a Pro or Business plan. ## Events Each event name is `.`: | Operation type | Events | | - | - | | `backup` | `backup.succeeded`, `backup.failed`, `backup.canceled` | | `restore` | `restore.succeeded`, `restore.failed`, `restore.canceled` | | `clone` | `clone.succeeded`, `clone.failed`, `clone.canceled` | | `delete` | `delete.succeeded`, `delete.failed`, `delete.canceled` | | `recover` | `recover.succeeded`, `recover.failed`, `recover.canceled` | | `extension` | `extension.succeeded`, `extension.failed`, `extension.canceled` | Handle unknown event names gracefully; new operation types can add events. Events are sent only for final statuses, not for `queued` or `running`. An endpoint registered without `databaseId` receives events for every database in the account. An endpoint registered with `databaseId` receives events for that database only. ### Payload ```json theme={null} { "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "event": "backup.succeeded", "createdAt": "2026-10-14T09:02:00.000Z", "data": { "operationId": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "databaseId": "550e8400-e29b-41d4-a716-446655440000", "databaseName": "my-app", "status": "succeeded", "stage": "completed", "progress": 100, "error": null } } ``` `id` and `data.operationId` are the operation ID. Read the full operation, including its `result`, with [Get operation](/api-reference/operations/get). For a failed operation, `error` is `{"code": "...", "message": "..."}`. ### Request headers | Header | Value | | - | - | | `Content-Type` | `application/json` | | `User-Agent` | `DBHost-Webhooks/1.0` | | `DBHost-Event-Id` | Unique per event and endpoint. Retries of the same delivery reuse it; use it to ignore duplicates. | | `DBHost-Timestamp` | Unix time in seconds when this attempt was signed | | `DBHost-Signature` | `v1=` followed by a lowercase hex HMAC-SHA256 digest | ## Verify the signature DBHost signs each attempt with your endpoint's signing secret: 1. Build the signed string: the `DBHost-Timestamp` value, a `.`, then the raw request body exactly as received. 2. Compute HMAC-SHA256 over that string. Use the full secret, including the `whsec_` prefix, as the key. 3. Hex-encode the digest, prefix it with `v1=`, and compare it to `DBHost-Signature` in constant time. Verify against the raw body before parsing JSON. DBHost does not enforce a timestamp window; reject timestamps that are too old for your needs (for example more than five minutes) to limit replay. ```js theme={null} import { createHmac, timingSafeEqual } from "node:crypto"; export function verifyDbhostWebhook(rawBody, headers, secret) { const timestamp = headers["dbhost-timestamp"]; const signature = headers["dbhost-signature"] ?? ""; const expected = "v1=" + createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex"); const a = Buffer.from(expected); const b = Buffer.from(signature); const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp)); return a.length === b.length && timingSafeEqual(a, b) && ageSeconds <= 300; } ``` ## Delivery and retries * A `2xx` response within 10 seconds counts as delivered. Redirects are not followed and count as a failure. * A failed delivery is retried up to five attempts in total, with growing delays (about 30 seconds, then 1, 2 and 4 minutes, plus jitter). * A successful delivery resets the endpoint's `consecutiveFailures` to `0`. * After a delivery has used all attempts, DBHost disables the endpoint if it has 10 or more consecutive failed attempts. Its `enabled` becomes `false` and `disabledAt` is set. Rotating the secret enables it again. ## Endpoint URL rules * The URL must be an absolute `https://` URL without a username or password. * `localhost`, `*.localhost`, `*.local` and `*.internal` hostnames are rejected. * The hostname must resolve, and every address must be public. Private, loopback, link-local, carrier-grade NAT, multicast and documentation ranges are rejected. DBHost checks this again on every delivery. * Any `#fragment` is removed. ## List endpoints `GET /api/v1/webhooks` ```http theme={null} GET /api/v1/webhooks Authorization: Bearer ``` ```json 200 OK theme={null} { "data": [ { "id": "d4e5f6a7-0000-4000-8000-000000000003", "databaseId": null, "url": "https://hooks.example.com/dbhost", "enabled": true, "consecutiveFailures": 0, "disabledAt": null, "createdAt": "2026-10-14T09:00:00.000Z", "updatedAt": "2026-10-14T09:00:00.000Z" } ] } ``` The list includes disabled endpoints. The signing secret is never returned here. ## Create an endpoint `POST /api/v1/webhooks` | Field | Type | Required | Description | | - | - | - | - | | `url` | string | Yes | HTTPS URL that follows the [URL rules](#endpoint-url-rules) | | `databaseId` | string | No | Limit events to one database you own. Omit for all databases. | ```http theme={null} POST /api/v1/webhooks Authorization: Bearer Content-Type: application/json {"url":"https://hooks.example.com/dbhost"} ``` ```json 201 Created theme={null} { "data": { "id": "d4e5f6a7-0000-4000-8000-000000000003", "databaseId": null, "url": "https://hooks.example.com/dbhost", "enabled": true, "createdAt": "2026-10-14T09:00:00.000Z", "secret": "whsec_" } } ``` > [!WARNING] > > `secret` is shown only in this response and in the rotate response. Store it > securely; DBHost cannot show it again. ## Rotate the signing secret `POST /api/v1/webhooks/{id}/rotate` Creates a new signing secret. The old secret stops being used immediately, including for retries of earlier events. Rotating also re-enables a disabled endpoint and resets `consecutiveFailures` to `0`. No request body is needed. ```http theme={null} POST /api/v1/webhooks/{id}/rotate Authorization: Bearer ``` ```json 200 OK theme={null} { "message": "Webhook signing secret rotated.", "data": { "id": "d4e5f6a7-0000-4000-8000-000000000003", "databaseId": null, "url": "https://hooks.example.com/dbhost", "enabled": true, "updatedAt": "2026-10-14T10:00:00.000Z", "secret": "whsec_" } } ``` ## Disable an endpoint `DELETE /api/v1/webhooks/{id}` Disables the endpoint: it stops receiving events, `enabled` becomes `false` and `disabledAt` is set. The endpoint stays in the list. Returns `204 No Content` with an empty body. To enable it again, rotate its secret. ```http theme={null} DELETE /api/v1/webhooks/{id} Authorization: Bearer ``` ## Errors | Status | Code | Description | | - | - | - | | 400 | `VALIDATION_ERROR` | `url` is missing | | 400 | `INVALID_WEBHOOK_ENDPOINT` | The URL breaks a [URL rule](#endpoint-url-rules), or `databaseId` is not a database you own | | 400 | `INVALID_JSON` | Request body is not valid JSON | | 401 | `UNAUTHORIZED` | Missing or invalid API key | | 403 | `PLAN_API_ACCESS_REQUIRED` | The account is not on Pro or Business | | 403 | `KEY_SCOPE_DENIED` | The key does not have full account access | | 404 | `WEBHOOK_NOT_FOUND` | No endpoint with this ID in your account | | 413 | `PAYLOAD_TOO_LARGE` | Request body is larger than 64 KB | | 429 | `RATE_LIMITED` | Too many requests; retry after `Retry-After` seconds | | 500 | `WEBHOOK_ROTATION_FAILED` | The secret could not be rotated; retry | | 500 | `WEBHOOK_UPDATE_FAILED` | The endpoint could not be disabled; retry | The CLI equivalents are `dbhost webhooks list`, `dbhost webhooks create [database-id]` and `dbhost webhooks rotate|disable `. API keys are managed in [API keys](/dashboard/api-keys); see also [Authentication](/api-reference/authentication). # Backups, retention and deletion Source: https://docs.dbhost.app/backups-and-retention What DBHost backs up per environment, how long logical backups and snapshots are kept, and how deleted databases are recovered or purged. This page is the single reference for what DBHost keeps and for how long. ## Logical backups by environment DBHost takes logical backups with `pg_dump` and stores them off-host in AWS S3 (`eu-north-1`) as `.sql.gz` files. | Environment | Daily scheduled backup | | - | - | | **production** | Always daily | | **staging** | Daily by default; can choose none | | **development**, **other** | Choose none or daily when creating the database | | **Preview** | None by default; can choose daily | Scheduled backups run once a day at no fixed time. A `none` choice only turns off scheduled backups: owners and admins can still take an on-demand backup from the dashboard on every plan, or through the API and CLI on Pro and Business. ## How long things are kept | What | Retention | Can you restore it? | | - | - | - | | Logical backups (scheduled and on-demand) | 30 days | Yes, see [Restore from a DBHost backup](/guides/restore-from-backup) | | Host snapshots | The three newest | No. Infrastructure disaster recovery only | | Deleted database and its deletion backup | 7 days | Yes, recover it or download the deletion backup | Host snapshots are disk-level copies of the database server. They can contain data from any database on the host, including previews without daily backup, and they rotate by count rather than by age. ## Deleting a database When an owner deletes a database, DBHost first creates and verifies a deletion backup, then disconnects the database. For the next **7 days** the owner can: * recover the database from **Recently deleted** in the dashboard or with [`POST /api/v1/databases/{id}/recover`](/api-reference/databases/deleted), if the plan has a free database slot * download the deletion backup, regardless of quota After 7 days, DBHost deletes the database and its deletion backup permanently and automatically. This cannot be undone. ## What DBHost does not offer * Point-in-time recovery (PITR) * Restoring an uploaded SQL file or dump through DBHost; see [Restore your own dump](/guides/restore-your-own-dump) * Restoring from host snapshots on request See [Limits](/limits) for the full list. # DBHost CLI reference Source: https://docs.dbhost.app/cli Install the @dbhost-app/cli package and manage DBHost databases, backups, roles, extensions and access from the terminal on Pro and Business. DBHost CLI is published as `@dbhost-app/cli` for Pro and Business accounts. ## Install Run the CLI directly with `npx`: ```bash theme={null} npx @dbhost-app/cli@latest --help ``` Or install it globally: ```bash theme={null} npm install -g @dbhost-app/cli dbhost --help ``` DBHost CLI supports macOS and Linux with Node.js 18 or later. ## Authenticate Create an API key from **Settings** in the dashboard, then log in: ```bash theme={null} dbhost auth login --api-key dbh_your_api_key_here ``` The CLI stores the API key locally so you do not need to pass it on every command. Paid plans support two API key scopes: * `Selected databases` limits the key to only the databases you choose * `Full account access` can create and manage every database in your account `Selected databases` is the recommended default. A `Full account access` key is required for database creation, database recovery, collaborator/member/invitation administration, and webhook administration. Selected-database keys only list the databases they can access. If you use a database-specific command against an out-of-scope database, the API responds as if the resource were missing. If you prefer environment variables: ```bash theme={null} export DBHOST_API_KEY=dbh_your_api_key_here export DBHOST_API_URL=https://dbhost.app/api/v1 ``` > [!WARNING] > > Local tests and smoke runs for mutating commands must set `DBHOST_API_URL` to > an explicit local test server. They fail closed rather than using the > production default. This rule does not authorize a production CLI mutation. ## Common commands List databases: ```bash theme={null} dbhost databases list ``` Use the `id` column from the list output with `dbhost databases get `. Create a database: ```bash theme={null} dbhost databases create analytics-v2 --display-name "Analytics V2" ``` Or pass the display name as the second argument: ```bash theme={null} dbhost databases create analytics-v2 "Analytics V2" ``` Trigger a backup: ```bash theme={null} dbhost backups trigger ``` Download a backup file: ```bash theme={null} dbhost backups download 20260326_222551.sql.gz ``` Delete a backup file: ```bash theme={null} dbhost backups delete 20260326_222551.sql.gz --yes ``` Reset a database password: ```bash theme={null} dbhost databases reset-password ``` Repair ownership after a pg\_dump restore (fixes `permission denied` from the app role): ```bash theme={null} dbhost databases repair-ownership ``` Wait for an asynchronous operation: ```bash theme={null} dbhost operations wait ``` Manage lifecycle and environment metadata: ```bash theme={null} dbhost databases stop dbhost databases environment staging ``` Use `dbhost backups list ` to discover the `filename` value before you download or delete a specific backup. Downloads save to `./` by default; pass `--output` to write the file somewhere else. ## JSON output Add `--json` to any command for machine-readable output: ```bash theme={null} dbhost databases list --json dbhost backups list --json ``` ## Command reference Run `dbhost --version` and `dbhost --help` to see what your installed version supports. Some commands below, including `pgcrypto` for extensions, the `databases migration*` commands, `databases events`, `databases access-audit` and the additional-role credential commands, require CLI 0.3.7 or newer. * `dbhost auth login [--api-key KEY] [--base-url URL]` * `dbhost auth logout` * `dbhost databases list [--json]` * `dbhost databases get [--json]` * `dbhost databases extensions [--json]` * `dbhost databases extensions install|upgrade --idempotency-key KEY [--json]` * `dbhost databases extensions status [--json]` * `dbhost databases create [display-name] [--display-name "My App"] [--environment production|staging|development|other] [--scheduled-backup none|daily] [--json]` * `dbhost databases start|stop|restart [--json]` * `dbhost databases environment [--json]` * `dbhost databases limits [--json]` * `dbhost databases limits-set --idempotency-key KEY [--json]` * `dbhost databases timeouts [--json]` * `dbhost databases timeouts-set --idempotency-key KEY [--json]` * `dbhost databases migration [--json]` * `dbhost databases migration-request [second-public-ip] --ttl-minutes 10-120 [--connections 1|2] [--statement-seconds 60-1800] [--lock-seconds 1-30] --idempotency-key KEY [--json]` * `dbhost databases migration-credentials --show-secret [--json]` * `dbhost databases migration-revoke --yes [--json]` * `dbhost databases events [--limit 1-100] [--cursor CURSOR] [--json]` * `dbhost databases access-audit [--json]` * `dbhost databases reset-password [--idempotency-key KEY] [--json]` * `dbhost databases delete --yes --idempotency-key KEY [--no-wait] [--timeout SECONDS] [--poll-interval SECONDS] [--json]` * `dbhost databases recover --yes --idempotency-key KEY [--connection-cap 2-24] [--no-wait] [--timeout SECONDS] [--poll-interval SECONDS] [--json]` * `dbhost databases repair-ownership [--json]` * `dbhost backups list [--json]` * `dbhost backups trigger [--no-wait] [--timeout SECONDS] [--poll-interval SECONDS] [--json]` * `dbhost backups download [--output PATH] [--json]` * `dbhost backups delete [--yes] [--json]` * `dbhost operations get [--json]` * `dbhost operations wait [--timeout SECONDS] [--poll-interval SECONDS] [--json]` * `dbhost network list [--json]` * `dbhost network add [label] [--json]` * `dbhost network remove [--json]` * `dbhost members list [--json]` * `dbhost members add [--json]` * `dbhost members update [--json]` * `dbhost members remove [--json]` * `dbhost invitations list [--json]` * `dbhost invitations resend|revoke [--json]` * `dbhost webhooks list [--json]` * `dbhost webhooks create [database-id] [--json]` * `dbhost webhooks rotate|disable [--json]` * `dbhost managed create [--display-name "My App"] [--environment production|staging|development|other] [--scheduled-backup none|daily] --idempotency-key KEY [--json]` * `dbhost managed clone [--display-name "Clone"] --idempotency-key KEY [--json]` * `dbhost managed roles [--json]` * `dbhost managed credential --show-secret [--json]` * `dbhost managed rotate|revoke --idempotency-key KEY --confirm-disconnect [--json]` * `dbhost managed get-change [--json]` * `dbhost managed reconcile-change --confirm-disconnect [--json]` * `dbhost managed custom-limit|legacy-limit --idempotency-key KEY [--json]` * `dbhost managed custom-continue-limit|legacy-continue-limit [--json]` * `dbhost managed custom-rename --display-name "maintenance" [--json]` * `dbhost managed custom-roles [--json]` * `dbhost managed custom-rotate|custom-revoke --idempotency-key KEY --confirm-disconnect [--json]` * `dbhost managed custom-get-change [--json]` * `dbhost managed custom-reconcile-change --confirm-disconnect [--json]` * `dbhost managed custom-delete --yes [--json]` * `dbhost managed custom-create --display-name "maintenance" --access-profile custom|read_only|read_write --idempotency-key KEY [--json]` * `dbhost managed legacy-create --display-name "reader" --access-profile custom|read_only|read_write --idempotency-key KEY [--json]` * `dbhost managed custom-credential --show-secret [--json]` * `dbhost managed legacy-opt-in [--json]` * `dbhost managed legacy-roles [--json]` * `dbhost managed legacy-credential --show-secret [--json]` * `dbhost managed legacy-rotate|legacy-revoke --idempotency-key KEY --confirm-disconnect [--json]` * `dbhost managed legacy-get-change [--json]` * `dbhost managed legacy-reconcile-change --confirm-disconnect [--json]` * `dbhost managed legacy-remove [--json]` Notes: * `databases create --environment development|other` also needs `--scheduled-backup none|daily`; production always keeps daily backups. * `databases migration-request` defaults to `--connections 1 --statement-seconds 600 --lock-seconds 5`. * `databases limits-set` changes the server connection cap shared by all roles of one database, within the range `databases limits` reports as `allowedServerConnectionCap`. New databases start at their plan default: Free 2, Pro 3, Business 4, development 2. * `--access-profile read_only|read_write|custom` matches Read only, Read & write and Custom in the dashboard. * `` is the role's own PostgreSQL connection limit: 1-20 for `legacy-create`; `custom-create` accepts 1 or more and the API checks it against the database's role budget. All roles still share the server connection cap set with `databases limits-set`. * Managed commands return once; reuse the same idempotency key to reconcile. Exit codes: * `0`: Success * `1`: API, network or other runtime error * `2`: Usage or validation error; nothing was sent to the API With `--json`, errors are written to stderr as `{"error":{"code","message"}}`. `dbhost databases extensions --json` lists the supported extensions for one database. On a database with managed SQL roles, `install` and `upgrade` submit one asynchronous operation for `citext`, `pg_trgm`, or `pgcrypto` (`pgcrypto` needs CLI 0.3.7 or newer); use `status` with the returned operation ID. Keep the same idempotency key if the first response is uncertain. See [PostgreSQL extensions](/guides/postgres-extensions). `dbhost databases create` uses the single-login creation API. For a new database with separate managed SQL roles, choose the checkbox in the dashboard (all plans) or use the managed-create API with a full-account key on Pro or Business. The `dbhost managed custom-*` commands manage additional roles on a database with managed SQL roles; see [Database roles](/guides/database-roles). The CLI has no restore or database deletion command. Use the dashboard or the API; see [Restore from a DBHost backup](/guides/restore-from-backup). # DBHost core concepts: databases, pooling, backups, roles Source: https://docs.dbhost.app/concepts How DBHost works: shared PostgreSQL host, PgBouncer pooling on port 6432, backups, API keys, team roles and customer webhooks explained. ## Databases A database in DBHost is a PostgreSQL database with its own SQL access and PgBouncer pool slot. Customer databases share the same PostgreSQL host and its CPU, memory, and disk; separate SQL roles and database grants do not provide hard resource isolation. Owners can also share dashboard access with other DBHost users through database-level memberships. DBHost checks shared-node connection and resource headroom before adding a new database route. These checks can defer new database creation when the node is under pressure; they do not impose a hard per-database CPU, memory or disk quota on existing databases. When you create a database from the dashboard, DBHost generates an internal PostgreSQL identifier from your display name and account. With the optional **Managed SQL roles** choice, the database gets separate generated owner, runtime, and read-only usernames; the database name is not a role username. Without that choice, the legacy login uses the internal database name as its username. On paid plans the public API accepts a database name using lowercase letters, numbers, and hyphens; its managed-create endpoint requires a full-account API key. Dashboard lists can therefore contain: * **Owned databases** — full owner control * **Shared databases** — access granted by another owner, shown with your effective access role ## PgBouncer Every database connection goes through [PgBouncer](https://www.pgbouncer.org/), a lightweight connection pooler. This means: * **Lower overhead** — PgBouncer multiplexes many client connections onto fewer PostgreSQL backend connections * **Port 6432** — Applications connect on port 6432. The only exception is the short-lived [direct migration connection](/guides/direct-migrations) on `migrate.dbhost.app:5432` * **Transaction pooling** — Ordinary application queries use PgBouncer; session features and some migration tools need the direct migration connection instead * **Server connection cap** — PgBouncer opens at most the plan's default number of server connections per database (Free 2, Pro 3, Business 4), adjustable up to the plan maximum; see [Connection limits and timeouts](/guides/connection-limits-and-timeouts) * **Network policy enforcement** — per-database IP allowlists are applied at the PgBouncer layer * **Lifecycle coordination** — start, stop, restart, and password resets flow through PgBouncer as well as PostgreSQL ## Backups Backups use `pg_dump` piped through `gzip`. In production, DBHost stores durable logical backups off-host in AWS S3 `eu-north-1` as `.sql.gz` files, while the VPS only keeps temporary working files during backup creation. New production databases default to daily scheduled logical backup; staging also defaults to daily, while development and other databases require an explicit `none` or `daily` choice. A `none` choice disables only scheduled logical backups. Enabled normal history is retained for 30 days, with no guaranteed backup time. Existing databases keep their prior daily policy when their environment label changes. Preview CI databases have no daily logical backup by default and can choose daily instead. Authorized roles can also trigger backups manually from the dashboard or API. Backup files are named with UTC timestamps: `20260319_120000.sql.gz`. Internal/system databases are excluded from the scheduled run. Dashboard on-demand backups are available on every plan; API and CLI triggers require Pro or Business. The verified backup created for database deletion is an explicit exception to normal 30-day retention. It remains available for the seven-day deleted database recovery window. Afterwards, DBHost permanently deletes the database and its deletion backup automatically. See [Backups and retention](/backups-and-retention). Infrastructure VM backups and manually retained snapshots are separate disk copies. They may contain preview data even when daily logical backup is off and do not expire when a preview lease ends. DBHost retains the three newest manual tenant-node snapshots and removes older ones after a protected restart is committed and verified. This is a rolling count, not an age-based expiry for an individual preview. DBHost-produced backups can be restored in place into the same database; see [Restore from a DBHost backup](/guides/restore-from-backup). DBHost does not restore uploaded dumps; import them yourself with [psql or pg\_restore](/guides/restore-your-own-dump). ## User identity DBHost treats one verified primary email as one logical account. If Clerk issues a new user ID for the same verified primary email, DBHost reclaims the account instead of creating a second local user record. Databases, API keys, and subscriptions move to the new identity automatically. Historical audit-log actor IDs are preserved. ## API keys Pro and Business users can create API keys for programmatic access to the DBHost REST API. Each key has: * A **prefix** (visible, for identification) * A **hashed secret** (stored securely, shown only once when created or replaced) * A **label** (your description) * A **scope mode** (`all` or `selected`, shown in the UI as `Full account access` or `Selected databases`) * **Last used** timestamp Use API keys with the `Authorization: Bearer ` header. Selected-database keys only list and operate on the databases assigned to them. Full-account keys can create and manage every database in the owning account. ## Security boundaries DBHost hardens the path between the dashboard and the VPS agent in a few specific ways: * **Stored tenant database passwords are encrypted at rest** in the control-plane database * **The in-dashboard SQL Explorer is owner-only and read-only**, and runs through a dedicated PostgreSQL explorer role * **Customer-facing operations only act on registered customer databases**, so internal databases such as `dbhost` are outside that surface * **Public API routes enforce request budgets and request-size caps**, which makes direct HTTP abuse and noisy automation harder to sustain These controls are additive. Direct PostgreSQL connections still use the normal database credentials shown on the detail page, while the explorer keeps its own tighter boundary. ## Database collaboration Database access has its own role system, separate from the platform-wide `user` / `admin` role. | Database role | Access | | - | - | | **owner** | Full control, including delete, sharing, lifecycle, backups, read-only explorer, credentials, IP allowlist, and activity | | **admin** | Manage collaborators, lifecycle, credentials, backups, connection activity, and IP allowlist | | **developer** | View credentials and view/download backups; cannot trigger, restore, or delete backups | | **viewer** | View the detail page and connection activity, but cannot use the explorer, credentials, or write actions | Dashboard roles are separate from PostgreSQL roles such as owner, runtime and read-only; see [Database roles](/guides/database-roles). Existing DBHost users receive membership directly. For an unknown email, the pending-invitation flow stores a local DBHost row and sends a seven-day Clerk invitation. Acceptance during first sign-in is idempotent; database/role data is not stored in Clerk public metadata. ## Activity log Every significant action is recorded in the activity log with: * **Who** performed the action (user ID) * **What** action was taken (create, delete, reset\_password, etc.) * **Which** resource was affected (database name, user ID) * **When** it happened * **Details** (JSONB, action-specific context) Users can review account and database activity in the dashboard. Platform operators have a separate internal audit view for full-platform administration. ## System incidents Operational failures are tracked separately from actor-driven activity so DBHost can distinguish system health issues from user actions. This includes: * render/runtime failures * public API failures * agent reachability problems * backup failures or stale backups * watchdog-generated health incidents Platform operators use a private internal admin surface to inspect and manage these incidents. ## Customer operation webhooks Webhook v1 sends `.` events for backup, restore, clone, delete, recover, and extension operations; see [Webhooks](/api-reference/webhooks). Deliveries use a stable event ID and a timestamped HMAC-SHA256 signature. Secrets are encrypted at rest and shown only at create/rotate time. Endpoints must use public HTTPS. DBHost rejects localhost, private/link-local addresses, unsafe redirects, and DNS-rebinding targets. Deliveries use bounded retries with jitter and a delivery ledger; permanently failing endpoints can be disabled automatically. ## Platform roles DBHost also has two platform-wide roles: | Role | Access | | - | - | | **user** | Own databases, own activity history, own API keys, own settings | | **admin** | Everything above plus platform administration, moderation, health monitoring, and incident response | # Account and database activity log Source: https://docs.dbhost.app/dashboard/activity Review the activity log for your DBHost account and owned databases: who did what, when, and on which database, with search in the dashboard. The activity page is available at `/dashboard/activity`. It gives each user a searchable history of account-level actions and changes tied to databases they own. ## What appears here The table includes: * account changes tied to your user * operations on databases you own * API key events for your account * admin actions that directly affect your account or owned databases Examples include database creation and deletion, password resets, backup actions, API key changes, email changes, and plan/admin interventions recorded in the audit log. ## Filters Use the toolbar to narrow results by: * free-text search * resource type * action * owned database * date range Results are paginated and ordered newest first. ## Relationship to database detail pages Each database page also shows a smaller **Changes** section. On the Overview tab it lists the latest five with an **All activity** link to the Activity tab; on the Activity tab, **Full history** opens this page. Use the full activity page when you want: * a broader account timeline * action-level filtering * date-based investigation * a longer history than the compact database detail view ## Visibility rules The full activity page is owner-scoped. That means it is built around your own account and databases you own. Shared database collaborators can still see the detail page activity snapshot for databases they can access, but the full dashboard activity page is not a general cross-team audit console. # Create and manage DBHost API keys Source: https://docs.dbhost.app/dashboard/api-keys Create, scope, replace and revoke DBHost API keys on Pro and Business. Use selected-database keys for least privilege in CI and scripts. ## Creating an API key 1. Go to **Settings** in the sidebar 2. Click **Create Key** 3. Enter a label (e.g., "CI/CD pipeline", "Monitoring script") 4. Choose a scope 5. Copy the key immediately > [!NOTE] > > API key creation requires a Pro or Business plan. Free users will see an upgrade prompt instead of the create form. > [!WARNING] > > Each full API key secret is shown only once when created or replaced. DBHost stores only a hash plus the visible prefix, so the current secret cannot be recovered later. Save it in a secure location. ## Key scopes * `Selected databases (Recommended)` limits the key to only the databases you choose. It can list and manage only those databases. * `Full account access` can create and manage every database in your account. Selected-database keys cannot create new databases. If your automation needs to provision databases, create a full-access key instead. Selected-database keys also follow least-privilege behavior in the API and CLI: * `GET /api/v1/databases` returns only the selected databases * Database-specific routes outside scope return `DATABASE_NOT_FOUND` * Full-account-only routes like database creation return `KEY_SCOPE_DENIED` API key scope is immutable in v1. To change scope, revoke the key and create a new one. ## Replacing a key Click **Replace key** next to an existing key if you need a fresh secret without changing the label or scope bindings. * The old secret stops working immediately * The new secret is shown once right after replacement * The key keeps the same label and scope configuration ### Safe rotation checklist **Replace key** is an immediate cutover. Use it only when every consumer can be updated at the same time. If you need a zero-downtime rotation, create a second key with the same least-privilege scope, migrate the consumers, verify the new key, and then revoke the old key. 1. Inventory every script, environment variable, CI job, and secret store that uses the current key. 2. Prepare the destination for the new secret before creating or replacing the key. 3. Copy the new secret immediately and update every consumer. 4. Run a read-only command such as `dbhost databases list` from the real consumer environment. 5. Revoke the old key if you used an overlapping second key, then remove the old secret from temporary notes and local shells. If the required scope or selected-database bindings have changed, create a new key instead of using **Replace key**. ## Using API keys Include the key in the `Authorization` header: ```bash theme={null} curl -H "Authorization: Bearer dbh_abc123..." \ https://dbhost.app/api/v1/databases ``` Or use it with the [CLI](/cli): ```bash theme={null} dbhost auth login --api-key dbh_abc123... ``` ## Key format API keys have a visible **prefix** (e.g., `dbh_abc1`) for identification and a secret portion. In the settings page, you'll see the prefix, label, scope, and last-used timestamp. ## Revoking a key Click **Revoke** next to any key in the settings page. The key stops working immediately. # Database backups in the DBHost dashboard Source: https://docs.dbhost.app/dashboard/backups Trigger, download, delete and restore DBHost database backups from the dashboard, and see which environments get a daily logical backup. ## How backups work DBHost uses `pg_dump` piped through `gzip` to create compressed SQL backups and stores them off-host in AWS S3 `eu-north-1` as timestamped `.sql.gz` files. Whether a database gets a daily scheduled backup depends on its environment: production always does, staging does by default, development and other databases use the choice made at creation, and previews have none unless you choose daily. Logical backups are kept for 30 days. See [Backups and retention](/backups-and-retention) for the full policy. Scheduled jobs run throughout the UTC day with no single guaranteed backup time. The dashboard shows a backup as complete only after the file has been verified. ## Triggering a backup 1. Go to your database detail page and open the **Backups** tab 2. Click **Back up now**. The same button is on the **All backups** page The backup is queued as an operation. The dashboard polls its status and updates the list without requiring a page refresh. On-demand backups work on every plan, including when the scheduled backup choice is `none`. ## Viewing backup history The backups page shows all available backups with: * **Filename** (timestamp-based: `YYYYMMDD_HHMMSS.sql.gz`) * **Size** (compressed) * **Created at** (UTC) * **Actions** — download, delete and restore, depending on your role Developers can view and download backups. Owners and admins can trigger, delete and restore backups. Restoring a database with managed or additional roles, and cloning a backup, is limited to the database owner. ## Restoring from a backup Choose **Restore** on a verified backup to replace the database's current contents with that backup. DBHost restores into an isolated copy, verifies it, takes a fresh backup of the current database and then switches over. Open connections are dropped during the cutover. Databases with managed SQL roles and databases that opted in to additional roles restore from the same **Restore** action, available to the database owner on DBHost-produced verified backups. The dashboard reserves the restore and advances it until it finishes; keep the page open. Role usernames and passwords stay the same. If the restore stops, press **Restore** again to continue the same operation. The [managed restore API](/api-reference/databases/managed-restores) does the same from your own tooling. See [Restore from a DBHost backup](/guides/restore-from-backup) for every path. ## Cloning a backup On a database with managed SQL roles, the owner can choose **Clone to new database** on a verified backup. DBHost creates a separate database from that backup with its own credentials and leaves the original untouched. The clone counts toward your plan's database limit. ## Restore your own file DBHost does not restore uploaded `.sql`, `.sql.gz`, `.dump`, or `.backup` files. The upload endpoints return `409 CUSTOM_RESTORE_UPLOAD_DISABLED`. Import your own dump with `psql` or `pg_restore` using the **owner** role over verify-full TLS: ```bash theme={null} PGSSLMODE=verify-full PGSSLROOTCERT=system psql -h YOUR_HOST -p YOUR_PORT \ -U YOUR_OWNER_ROLE -d YOUR_DATABASE -v ON_ERROR_STOP=1 -f backup.sql PGSSLMODE=verify-full PGSSLROOTCERT=system pg_restore --no-owner --exit-on-error \ -h YOUR_HOST -p YOUR_PORT -U YOUR_OWNER_ROLE -d YOUR_DATABASE backup.dump ``` [Restore your own dump](/guides/restore-your-own-dump) covers `.sql.gz` files, grants and ownership repair. ## Downloading and deleting backups * Use **Download** to fetch the exact `.sql.gz` file. The file may be served directly from S3 through a short-lived link. * Use **Delete** to remove a backup file from the available backup list. Older S3 versions may remain until storage retention expires. > [!WARNING] > > Deleting a backup cannot be undone from the dashboard or API. ## Deleted databases When a database is deleted, its verified deletion backup stays downloadable from **Recently deleted** for seven days. After that, the database and the deletion backup are deleted permanently and automatically. # Connection history and access audit Source: https://docs.dbhost.app/dashboard/connection-history See verified PgBouncer connection events for 7 days, daily totals for 30 days and owner access changes for 90 days on each DBHost database. The **Activity** and **Access** tabs on the database page show who connected and who changed access. Three views cover different time spans: | View | Retention | What it shows | | - | - | - | | **Connections** (Activity tab) | 7 days | Individual PgBouncer events: **Attempted**, **Signed in**, **Refused**, **Disconnected**, with role and TLS status | | **Daily totals** (Activity tab, under Connections) | 30 days | Daily counts per outcome and TLS status | | **Access changes** (Access tab) | 90 days | Owner-initiated role, credential, member and network changes | **Attempted** does not prove a successful login; look for **Signed in** or **Refused** to see the outcome. ## Sampled clients are separate **Sampled live clients** on the Activity tab shows a sample of live PgBouncer clients (username, client IP, application name and time). It is sampled, not a complete log, and is shown separately from the verified events. ## What is not shown * SQL text, passwords, tokens and TLS ciphers * Logins that DBHost cannot attribute to this database * Access changes made by collaborators, and older unattributed events * Sessions on the [direct migration connection](/guides/direct-migrations) ## Incomplete history DBHost collects events from PgBouncer logs. If collection was interrupted, the page says **Some events may be missing.** Stopping a database, or deleting it and recovering it within seven days, does not by itself lose the events that were made while it was running; an event that could not be attributed within ten minutes of happening is not shown. Even without a recorded gap, the history is not a guaranteed complete record of every connection. ## API and CLI On Pro and Business: * [`GET /api/v1/databases/{id}/connection-events`](/api-reference/databases/connection-events) * [`GET /api/v1/databases/{id}/access-audit`](/api-reference/databases/access-audit) * `dbhost databases events ` and `dbhost databases access-audit ` in CLI 0.3.7 or newer # Manage PostgreSQL databases in the DBHost dashboard Source: https://docs.dbhost.app/dashboard/databases Create, connect, share, stop, delete and recover DBHost databases from the dashboard, and find each setting on the database detail page. ## Creating a database 1. Click **New database** from the dashboard 2. Enter a **Name**. DBHost generates the internal PostgreSQL identifier 3. Choose an **Environment** and, where offered, **Backups** (daily or off). Production always keeps daily backups; see [Backups and retention](/backups-and-retention) 4. Under **Database logins**, keep **Separate logins** (owner, runtime and read-only) or choose **One owner login** 5. Check the summary on the right and click **Create database** DBHost then opens the database page with readiness state, the connection string and examples for `psql`, Node.js, Python, Prisma and Drizzle. ## SQL roles Managed SQL roles are available on every plan for a new database. The owner login makes schema changes; runtime and read-only have narrower privileges. The owner can reveal or rotate each credential and add up to ten additional roles with a **Read only**, **Read & write** or **Custom** profile. Existing single-login databases keep their current login and can opt in to additional roles from the detail page. After opt-in, DBHost-produced backups of the same database can be restored in place. Cloning a database that has additional roles is rejected with `409 LEGACY_OVERLAY_REQUIRES_V4`. See [Database roles](/guides/database-roles) for profiles, grants and the rotate, revoke and remove actions. ## Dashboard list The main dashboard shows every database you can access, as **Cards** or a **List**. Your choice is remembered for your account. * Owned databases appear normally * Shared databases stay in the same list and include your effective access role * Search and status filters help when the list grows * **Active** and **Recently deleted** views separate live databases from deleted ones that can still be recovered * Environment filters use one optional value: `production`, `staging`, `development`, or `other` ## Database detail page Click any database to open its page. The header shows the name, status and internal name, **Open SQL Explorer**, and a **Manage** menu with backups, activity, lifecycle, password reset and owner-only deletion. The page is split into tabs, each with its own link (`?tab=`): | Tab | What it shows | | - | - | | **Overview** | Alerts, the connection string, storage and connections at a glance, recent changes | | **Connect** | Every connection field with Copy, examples for `psql`, Node.js, Python, Prisma and Drizzle, TLS and the direct migration connection | | **Roles** | Logins and extra roles, or additional roles on an existing single-login database; only where role management is available | | **Backups** | Schedule, last backup, **Back up now**, recent backups and the preview lease; only for roles that can see backups | | **Performance** | Usage, connection limit, query timeouts and extensions | | **Access** | Team, allowed IPs and access changes | | **Activity** | Connections, sampled live clients and changes | Technical details such as the requested and loaded connection limit or platform limits sit behind **Show details**. Errors and pending changes are always shown. DBHost stores tenant database passwords encrypted at rest. When you reveal or reset a password, DBHost decrypts or replaces it for the authorized session. Connections require TLS; use `sslmode=verify-full`, see [TLS connections](/guides/tls-connections). ## Connection limit and query timeouts On the **Performance** tab, the owner can change the **Connection limit** (the server connection cap: plan default, adjustable up to the plan maximum, shared by all roles; the dashboard offers 2 up to your plan maximum) and the **Query timeouts** profile (Standard, Balanced or Short). Neither is a CPU, memory or disk quota. For owners, **New-session timeout defaults by role** shows the PostgreSQL catalog value each managed login inherits for query, lock and idle timeouts, including role-specific overrides. It does not measure sessions already open or settings an application changes with `SET`. A missing catalog value is shown as an unread server default. PostgreSQL applies [role and database defaults](https://www.postgresql.org/docs/16/sql-alterrole.html) at login; choosing a DBHost profile does not remove role overrides. See [Connection limits and timeouts](/guides/connection-limits-and-timeouts). ## Extensions The **Performance** tab shows **Extensions** for the owner of an active database. On databases with managed SQL roles, the owner can also install or upgrade `citext`, `pg_trgm` and `pgcrypto`. Each one is marked **Supported**, **Testing** or **Not yet supported**. See [PostgreSQL extensions](/guides/postgres-extensions). ## Direct migration connection For a database with managed SQL roles, the owner can request short-lived direct access on `migrate.dbhost.app:5432` for migration tools under **Schema migrations** on the **Connect** tab. See [Direct database migrations](/guides/direct-migrations). ## Preview lease A preview database shows a short notice on **Overview** and a **Preview** panel on the **Backups** tab with its expiry, backup choice and, after deletion, its recovery deadline. See [Preview databases](/guides/preview-databases). ## Team access The **Access** tab has a **Team** section. Owners and database admins can **Invite** by email and assign one of these roles: | Database role | Access | | - | - | | **admin** | Manage collaborators, lifecycle, credentials, backups, connection activity, and IP allowlist | | **developer** | View credentials and view/download backups; cannot trigger, restore, or delete backups | | **viewer** | View the detail page and connection activity, but cannot use the explorer, credentials, or write actions | An existing DBHost user receives membership directly. An unknown email gets a pending invitation with a seven-day expiry; resending replaces the previous invitation. Acceptance during first sign-in is idempotent. The owner is always shown separately and cannot be removed or downgraded through the member flow. Team access controls the dashboard, not PostgreSQL privileges; for those, see [Database roles](/guides/database-roles). Removing or lowering a member's role changes their DBHost dashboard access. It does not invalidate a shared SQL password they may have copied. To stop direct SQL access, the database owner must plan a password rotation and update apps or other people that use that credential. ## Allowed IPs The **Allowed IPs** section on the **Access** tab controls which client networks can connect through PgBouncer. * Add an **IP range** such as `203.0.113.0/24`, with an optional label * If the list is empty, the database accepts connections from any IP * As soon as you add one or more rules, only matching networks can connect * **Remove** asks for confirmation and warns when it is the last rule These rules affect database connections through port `6432`. They do not change dashboard sign-in behavior. ## Start, stop, and restart The **Manage** menu on the detail page includes lifecycle controls when your role allows it. * **Start** re-enables a stopped database and waits until pooled client connections are usable again * **Stop** asks for confirmation, then blocks new connections, ends active sessions, and moves the database into the `stopped` state * **Restart** refreshes the pooled runtime without deleting the database Stopping a database is operational, not destructive. Data remains on disk. ## Activity The **Activity** tab shows **Connections** (verified events for 7 days, with daily totals behind a disclosure), **Sampled live clients** (a sample of PgBouncer clients with username, client IP, application name and time) and **Changes** with a link to the full history. See [Connection history](/dashboard/connection-history). ## SQL Explorer Each database has an **Open SQL Explorer** button in its header. The owner can browse `public` tables and run one read-only `SELECT` or `EXPLAIN SELECT` statement at a time. See [Data Explorer](/dashboard/explorer) for the editor, saved queries and guardrails. ## Resetting a password If you need a new password: 1. Go to the database detail page and open **Manage** 2. Click **Reset password** and confirm 3. A new password is generated and the old one stops working immediately On a database with managed SQL roles, rotate the specific role instead; see [Database roles](/guides/database-roles). > [!WARNING] > > All active connections using the old password will be disconnected. Update > your application's connection string before resetting. ## Deleting a database Owners can select **Delete database** from the database's **Manage** menu. Database admins, developers and viewers cannot delete a database. The confirmation dialog requires the exact internal database name. DBHost then creates and verifies a deletion backup, disconnects the database and moves it to **Recently deleted**. > [!CAUTION] > > Deletion immediately disconnects the database. Confirm the exact internal > name and make sure dependent applications are prepared before continuing. ## Deleted database recovery For seven days after deletion, the **Recently deleted** card shows the deletion time, the permanent-deletion deadline and the verified deletion backup. The owner can: * download the deletion backup, regardless of quota * recover the database when an active database slot is available Deleted databases do not count toward active quota but keep their name reserved. Recovery restores the previous team memberships, IP allowlist and still-existing selected-database API-key bindings. Missing users or keys are skipped with a warning; pending invitations are not restored. A recovered preview starts a new 24-hour lease. After seven days, DBHost permanently deletes the database and its deletion backup automatically. See [Backups and retention](/backups-and-retention). ## Database statuses | Status | Meaning | | - | - | | **active** | Running and accepting connections | | **creating** | Being provisioned (usually seconds) | | **stopped** | Provisioned but not accepting new pooled connections | | **deleting** | Being removed | | **error** | Something went wrong during provisioning | # Data Explorer: read-only SQL in the dashboard Source: https://docs.dbhost.app/dashboard/explorer Browse public tables and run guarded read-only SELECT and EXPLAIN queries on your DBHost database from the dashboard, with saved queries. The Data Explorer is available from each database detail page at `/dashboard/{id}/explorer`. It is designed for quick inspection inside the dashboard, without replacing your normal database client. On the database detail page, DBHost also exposes a dedicated **SQL Explorer** action card so the query workspace stays prominent. ## Database context At the top of the explorer, DBHost shows the current database context directly in the header card and includes a database switcher for every database you own that supports explorer access from the dashboard. * The explorer is always connected to one selected database at a time * Queries in the SQL editor already run against that selected database * You do not need to prefix the database name in `FROM` * If you want another database, switch explorer context first Explorer access is limited to the database owner. Public API key scope does not control the in-dashboard explorer. ## Tabs ### Tables The **Tables** tab lists public tables in the selected database and lets you preview rows with pagination. * Only tables in the `public` schema are shown * Row previews are paginated in the UI * Table browsing is scoped to the database opened from the dashboard ### SQL Query The **SQL Query** tab runs a single guarded read-only statement at a time. It now includes an editor-first workflow built from live explorer context: * An editor-first layout with query results directly below the SQL surface * A companion utility rail with three panes: **Suggested**, **Saved**, and **Recent** * Selected table context in the rail, including a live table picker and quick table chips * One-click starter queries for previewing rows, counting rows, distinct previews, and `EXPLAIN` plans * A slash command drawer inside the SQL editor when you type `/` * Context-aware table completion when you use `/` after `FROM` or `JOIN` * A quick `FROM ` action in the editor footer for simple `SELECT` statements * A per-device editor settings menu so you can toggle slash commands, `FROM` / `JOIN` table suggestions, and the helper rail * A safe `psql`-style inspect subset: `\dt`, `\dv`, `\di`, and `\d ` (the explorer also accepts `/dt`, `/dv`, `/di`, and `/d `) Inside the SQL editor, `FROM` expects a table name, not a database name. The explorer already chooses the database connection from the header switcher above. The inspect commands above are explorer-native shortcuts, not raw SQL. They are resolved by the explorer before SQL parsing and stay limited to the selected database and the `public` schema. Accepted inspect examples: * `\dt` * `\dv` * `\di` * `\d "analytics_downloads"` * `\d FROM "analytics_downloads"` * `/dt FROM "analytics_downloads"` When you describe a relation with `\d` or `/d`, the result view shows: * Columns, data types, nullability, and defaults in the main result grid * Supplemental sections for indexes and relation metadata when available ### Saved, recent, and persistence The helper rail keeps non-editor tasks out of the main writing surface: * **Suggested** contains starter snippets based on the currently selected table * **Saved** lets you save named SQL presets for the current database, mark favorites, edit them, and delete them * **Recent** keeps the latest query history first for the current database and lets you tune the retention count for your account The **Saved** pane highlights favorites first, then keeps the full saved-query list underneath. The **Recent** pane always shows the newest runs first, and loading one of them replaces the SQL currently in the editor so you can rerun or modify it. Saved presets and recent history are stored on the server per user and per database. ### Keyboard shortcuts The editor supports these shortcuts: * `/` opens the command drawer when slash commands are enabled * `Esc` closes the open drawer or suggestion list * `Ctrl+Enter` or `Cmd+Enter` runs the current query * `Ctrl+C` clears the editor when no text is selected Allowed statement types: * `SELECT` * `EXPLAIN SELECT` Additional rules: * Only one statement is allowed per run * The explorer is limited to the selected database and the `public` schema * System catalogs, system schemas, utility commands, and data-modifying statements are blocked ## What the explorer blocks The explorer blocks destructive or escape-hatch SQL such as: * `DROP` * `TRUNCATE` * `ALTER` * `INSERT` * `UPDATE` * `DELETE` * `CREATE TABLE` * `CREATE INDEX` * multi-statement batches * transaction/session control statements * explicit system-schema access > [!NOTE] > > Explorer guardrails only apply here. Direct PostgreSQL connections still use > your normal database credentials and privileges. # Account, billing and plan settings Source: https://docs.dbhost.app/dashboard/settings Manage your DBHost account, compare Free, Pro and Business plan limits, upgrade or downgrade your plan and find your API key settings. ## Account Your account is managed through Clerk. Click the user avatar in the sidebar to: * Update your profile (name, avatar) * Change your email address * Change your password * Manage connected accounts (Google, etc.) DBHost treats one verified primary email as one logical account. If your Clerk user ID changes but your verified primary email stays the same, DBHost reclaims your databases, API keys, and subscription onto the new identity automatically. ## Billing The settings page shows your current plan and subscription status. | Plan | Price | Databases | Storage per database | API Access | | - | - | - | - | - | | Free | \$0/mo | 1 | 250 MB | No | | Pro | \$5/mo | 5 | 1 GB | Yes | | Business | \$15/mo | 20 | 2 GB | Yes | Free, Pro, and Business are the only self-service app plans. The app loads the active paid prices from Stripe and verifies the Business catalog entry at \$15/month. Plan changes use an invoice preview and Stripe-confirmed entitlements. Downgrades take effect at period end and are blocked while current resource usage exceeds the target plan; DBHost does not automatically delete databases to make a downgrade fit. ## API keys See [API keys](/dashboard/api-keys) for details on creating, using, and revoking keys. > [!NOTE] > > API access requires a Pro or Business plan. Free plan users will see a prompt > to upgrade when trying to generate API keys. Paid users can create either: * `Selected databases (Recommended)` keys for least-privilege automation * `Full account access` keys for workflows that need to create or manage every database Keys can also be replaced from the settings page if you need a new secret. Replacing a key invalidates the old secret immediately and shows the new one once. API key scope is immutable in v1. To change which databases a key can use, revoke it and create a new one. # Automate DBHost with the CLI and REST API Source: https://docs.dbhost.app/guides/cli-api-automation Automate DBHost database tasks from scripts, CI and terminals on Pro and Business with scoped API keys, the dbhost CLI and JSON output. DBHost is useful when the same database actions keep repeating: list databases, create a new one for a project, trigger backups, or reset a password after a release. This guide shows the shortest path to automating those tasks on Pro and Business with the published CLI and the REST API. **Best fit:** Indie developers, small SaaS teams, and agencies that want repeatable database workflows from a terminal, CI job, or deployment script. **Not for:** Teams that only need one-off dashboard actions and do not need API access or automation yet. ## What you need * A DBHost account * API access on a Pro or Business plan * One API key with the smallest scope that fits the job > [!TIP] > > Use a selected-database key by default. Switch to full-account access only > when the job must create databases or act across the whole account. ## 1. Create an API key Create the key in **Settings** and copy it immediately. DBHost only shows the full secret once. If the automation only needs to inspect or operate on an existing database, select that database and keep the key scoped narrowly. ## 2. Use the CLI locally Install or run the published CLI: ```bash theme={null} npx @dbhost-app/cli@latest --help ``` Then log in: ```bash theme={null} dbhost auth login --api-key dbh_your_api_key_here ``` The CLI stores the key locally, so you do not need to pass it every time. ## 3. Use environment variables in CI For scripts and CI jobs, prefer environment variables instead of interactive login: ```bash theme={null} export DBHOST_API_KEY=dbh_your_api_key_here export DBHOST_API_URL=https://dbhost.app/api/v1 ``` With those set, the CLI can run non-interactively from shell scripts, GitHub Actions, or scheduled jobs. ## 4. Automate the common database workflows List databases: ```bash theme={null} dbhost databases list --json ``` Create a database: ```bash theme={null} dbhost databases create analytics-v2 --display-name "Analytics V2" --json ``` Trigger a backup: ```bash theme={null} dbhost backups trigger --json ``` List, download, or delete a backup file: ```bash theme={null} dbhost backups list --json dbhost backups download 20260326_222551.sql.gz --output ./latest-backup.sql.gz dbhost backups delete 20260320_030000.sql.gz --yes --json ``` Without `--output`, `dbhost backups download` saves the file to your current working directory using the same backup filename. Reset a password: ```bash theme={null} dbhost databases reset-password --json ``` The `--json` flag is the safest default for automation because it gives you machine-readable output that can be parsed by your job runner. ## 5. Call the API directly when HTTP is simpler If your system already works with cURL or fetch, call the API directly: ```bash theme={null} curl -H "Authorization: Bearer $DBHOST_API_KEY" \ https://dbhost.app/api/v1/databases ``` For a create flow, send JSON to the same API the CLI uses: ```bash theme={null} curl -X POST https://dbhost.app/api/v1/databases \ -H "Authorization: Bearer $DBHOST_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"analytics-v2","displayName":"Analytics V2"}' ``` The CLI covers the most common workflows. The REST API covers more, such as restore, deletion and recovery, and preview databases. Use the CLI where it has a command and the API for the rest. ## 6. Handle retries without changing your workflow The CLI commands and REST endpoints use the same authentication model and success payloads. Current request controls return: * `413 PAYLOAD_TOO_LARGE` when a JSON body is too large * `429 RATE_LIMITED` when a route is being hit too aggressively or when there are too many unauthorized attempts If you call the API directly, respect the `Retry-After` header on `429` responses before retrying. If you use the CLI, treat `429` as a temporary backoff condition instead of a permanent failure. ## 7. Keep automation safe * Default to selected-database keys * Use full-account keys only for database creation or account-wide administration * Treat database IDs as UUIDs, not row numbers * Revoke keys you no longer need * Keep secrets in CI secret storage, not in source control * Keep create and mutation jobs bounded; avoid tight retry loops without delay * Respect `Retry-After` when the API returns `429` > [!NOTE] > > The dashboard covers every action on every plan. The API adds restore, > deletion and recovery for automation on Pro and Business. The CLI has no > restore or database deletion command. ## Example CI job ```yaml theme={null} name: Trigger backup on: workflow_dispatch: schedule: - cron: "0 3 * * *" jobs: backup: runs-on: ubuntu-latest steps: - name: Trigger DBHost backup env: DBHOST_API_KEY: ${{ secrets.DBHOST_API_KEY }} run: npx -y @dbhost-app/cli@latest backups trigger "${{ secrets.DBHOST_DATABASE_ID }}" --json ``` That pattern keeps the job simple: the secret stays in the CI system, the command stays readable, and the output is easy to log or inspect. ## Next steps > **[Start for Free](https://dbhost.app/sign-up?utm_source=docs\&utm_medium=guide\&utm_campaign=cli_api_automation\&utm_content=start_for_free)** > > Create an account now, then move to a paid plan when you need API access. > **[API Reference](/api-reference/introduction)** > > Use the REST API directly when HTTP fits your workflow better than the CLI. * Read the [CLI](/cli) reference for the full command list. * Read the [API introduction](/api-reference/introduction) for the request and response shape. * Read [API keys](/dashboard/api-keys) if you want a deeper look at scopes and revocation. # Connection limits and query timeouts Source: https://docs.dbhost.app/guides/connection-limits-and-timeouts How DBHost's per-database PgBouncer server connection cap (plan default, adjustable up to the plan maximum) and the three fixed query timeout profiles work. Every DBHost database sits behind PgBouncer in transaction pooling mode. Two per-database settings shape how your queries share the PostgreSQL host: the server connection cap and the query timeout profile. Neither is a CPU, memory or disk quota; customer databases share the same host resources. ## Server connection cap The cap is the number of PostgreSQL server connections PgBouncer may open for your database at the same time. * **Default:** Free 2, Pro 3, Business 4; development databases start at 2 * **Range:** 2 up to the plan maximum: Free 4, Pro 12, Business 24 * **Shared:** by every role on the database, including additional roles A cap that is already above your plan's maximum, for example after a plan change, is kept. You can lower it, but not raise it further. Many application clients can connect to PgBouncer at once. When all server connections are busy, new transactions wait in PgBouncer's queue until one is free. The database detail page shows current server connections and waiting clients, and warns at 80% of the cap or when clients are waiting. Raise the cap when clients wait regularly. Lower it to leave more room for other workloads on a busy app. To change it: * Dashboard: **Performance** tab → **Connection limit** → **Change limit** * API: [`POST /api/v1/databases/{id}/connection-limits`](/api-reference/databases/connection-limits) * CLI: `dbhost databases limits-set --idempotency-key KEY` The dashboard, API and CLI accept every value from 2 up to your plan's maximum (caps above 8 through the CLI need CLI 0.3.8 or newer). Lowering the cap does not disconnect existing sessions. A request above your plan's maximum returns `403 CONNECTION_CAP_ABOVE_PLAN`. An increase within the plan can still be refused with `503 CLUSTER_CAPACITY_FULL` when measured use on the shared host is close to full; the cap is then unchanged. API and CLI access require Pro or Business. ## Query timeout profiles A profile sets PostgreSQL defaults for new sessions on your database. There are three fixed profiles: | Profile | Statement | Lock wait | Idle in transaction | | - | -: | -: | -: | | **Standard** | 60 s | 10 s | 30 s | | **Balanced** | 30 s | 3 s | 15 s | | **Short** | 15 s | 1 s | 5 s | Use **Standard** for most apps, **Balanced** for interactive web traffic, and **Short** when slow queries should fail fast. Running transactions are not interrupted; PgBouncer moves to the new defaults as server connections are replaced. If a role has its own timeout overrides, those overrides still apply, and the dashboard points this out. To change the profile: * Dashboard: **Performance** tab → **Query timeouts** → **Change timeout profile** * API: [`POST /api/v1/databases/{id}/query-timeouts`](/api-reference/databases/query-timeouts) * CLI: `dbhost databases timeouts-set --idempotency-key KEY` Long migrations that exceed these limits can run through the [direct migration connection](/guides/direct-migrations), which has its own statement and lock timeouts. # Database roles: owner, runtime and additional roles Source: https://docs.dbhost.app/guides/database-roles Use DBHost managed SQL roles: owner for migrations, runtime for your app, read-only for reporting, plus up to ten additional roles per database. With **Managed SQL roles**, each database gets separate PostgreSQL logins instead of one shared password. Choose the option when you create a database in the dashboard (every plan), or use the [managed-create API](/api-reference/databases/managed-create) on Pro or Business. ## Base roles | Role | Use it for | Privileges | | - | - | - | | **owner** | Migrations and schema changes | Owns the schema and objects; can run DDL and grant access | | **runtime** | Your application | Reads and writes data; cannot run DDL | | **read-only** | Reporting, BI and support queries | `SELECT` only | Each role has its own generated username and password. The database name is not a username. Reveal or rotate one credential at a time on the database detail page; rotating one role does not change the others. Rotation and revocation close that role's open connections. See [Managed role changes](/api-reference/databases/role-changes) for the API. ## Additional roles You can add up to **10 additional roles** per database, for example a separate login for a worker, an analytics tool or a contractor. Pick one access profile when you create the role: | Profile | What DBHost grants | | - | - | | **Read only** | `CONNECT` plus `SELECT` | | **Read & write** | `CONNECT` plus DML: `SELECT`, `INSERT`, `UPDATE` and `DELETE` | | **Custom** | `CONNECT` only. The role can log in immediately but sees no tables until your migrations grant access | DBHost never gives an additional role `CREATE`, `TEMPORARY`, superuser, `CREATEDB`, `CREATEROLE` or replication privileges. Each role has its own connection limit, and all roles share the database's [server connection cap](/guides/connection-limits-and-timeouts). ### Grant access to a Custom role Run grants as the **owner** role, ideally in a migration so they are versioned with your schema. Replace `reporting_role` with the generated username shown in the dashboard: ```sql theme={null} GRANT USAGE ON SCHEMA public TO reporting_role; GRANT SELECT ON public.orders, public.customers TO reporting_role; -- Tables the owner creates later get the same access automatically. ALTER DEFAULT PRIVILEGES FOR ROLE CURRENT_USER IN SCHEMA public GRANT SELECT ON TABLES TO reporting_role; ``` `ALTER DEFAULT PRIVILEGES` only applies to objects created later by the role that runs it, so run it as the owner role that runs your migrations. ## Manage additional roles On the database page, open the **Roles** tab. Under **Extra roles** (or **Additional database roles** on an existing single-login database), the owner can: * **Add** a role with a name, access profile and connection limit * **Reveal** its password when you need to configure a client * **Rotate** its password; open sessions are disconnected * **Revoke** it; new logins and active sessions are denied * **Remove** a revoked role to free its slot These actions are available in the dashboard on every plan. The [API](/api-reference/databases/additional-roles) and the [CLI](/cli) (`dbhost managed custom-*` commands) require Pro or Business. ## Existing databases A database created before managed SQL roles keeps its single login. The owner can opt in to additional roles from the **Roles** tab. The existing login, password and connection string stay unchanged. Backups made before opt-in do not include the additional roles. After opt-in, DBHost-produced backups of that database can be restored in place; see [Restore from a DBHost backup](/guides/restore-from-backup). Cloning a database that has additional roles is not offered and returns `409 LEGACY_OVERLAY_REQUIRES_V4`. ## Dashboard team access is separate SQL roles control what a PostgreSQL login can do. Dashboard [team access](/dashboard/databases#team-access) controls what another DBHost user can see and change in the dashboard. The two are independent. # Run schema migrations over a direct connection Source: https://docs.dbhost.app/guides/direct-migrations Run Prisma, Drizzle, Flyway, Rails or Django migrations over a short-lived direct TLS connection on migrate.dbhost.app:5432 while your app keeps PgBouncer. Migration tools often need features that transaction pooling does not support, such as session-level advisory locks. Databases with [managed SQL roles](/guides/database-roles) can request a short-lived direct connection that bypasses PgBouncer. ## Request access Request access from the database page under **Direct migration connection**, through the [API](/api-reference/databases/migration-connection), or with `dbhost databases migration-request` (CLI 0.3.7 or newer). Provide: * one or two **exact public IP addresses** of your migration runner; no CIDR ranges or wildcards * a **lease**: 30, 60 or 120 minutes in the dashboard, or 10–120 minutes through the API and CLI * **1 or 2 concurrent sessions** DBHost reserves the request, checks the database, the PostgreSQL TLS certificate and the session budget, opens the network path for your exact IPs only, and then enables the migration login. This usually takes under a minute, and the dashboard follows the steps (**Preparing access**, **Opening the network path**, **Active until …**) without a reload. The lease's time starts when the login is prepared, so waiting never shortens it. If access cannot be opened safely, the request closes and the dashboard says why; request again. ## Connect The active profile shows `migrate.dbhost.app:5432`, database name, a dedicated migration username and expiry. Use `sslmode=verify-full` and the password revealed by the owner in the dashboard or returned by `dbhost databases migration-credentials --show-secret` and the owner-scoped API. See [TLS connections](/guides/tls-connections) for client certificate settings. Keep that password in your CI secret store; never commit it or put it in a command URL. The migration login starts with the database owner as its effective SQL role, so migration tools create objects under the same owner as the rest of the managed database. It does this through `SET role`: `current_user` returns the owner role, while `session_user` returns the migration login `dbr__migr`. Both are expected. It cannot create roles or databases and allows at most the one or two sessions you requested. Your application runtime URL stays on PgBouncer at port 6432. | Tool | Migration process | Important detail | | - | - | - | | Prisma Migrate v7 | Put the direct profile in `prisma.config.ts` `datasource.url`; keep your application adapter on its normal pooled URL. | `migrate deploy` and `resolve` use the migration URL. `migrate dev` may require a separate, precreated shadow database; this role has no `CREATEDB`. [Prisma configuration](https://www.prisma.io/docs/orm/v7/reference/prisma-config-reference) | | Drizzle Kit | Point the migration-specific `drizzle.config.ts` `dbCredentials.url` to the direct profile. | `drizzle-kit migrate` owns its migration ledger. Serialize runners in CI. [Drizzle Kit migrate](https://orm.drizzle.team/docs/drizzle-kit-migrate) | | Flyway 13.6 | Select **2 concurrent direct sessions** in the dashboard or use `--connections 2` with the CLI, and set `flyway.postgresql.transactional.lock=false` for the migration process. | The default lock mode exhausted this profile's two-session limit in a PostgreSQL 16.15 qualification. The setting uses a session-level advisory lock and also permits a nontransactional `CREATE INDEX CONCURRENTLY` migration. Qualify other Flyway versions before relying on the same limit. [Flyway lock setting](https://documentation.red-gate.com/flyway/reference/configuration/flyway-namespace/flyway-postgresql-namespace/flyway-postgresql-transactional-lock-setting) | | Rails | Run `db:migrate` with the direct profile, separate from the app's pooled configuration. | Keep Active Record advisory locks enabled for migrations. Use `disable_ddl_transaction!` only when the migration requires nontransactional DDL. [Rails configuration](https://guides.rubyonrails.org/configuring.html) | | Django | Run `manage.py migrate` against a direct database alias or migration-only settings. | PostgreSQL migrations are atomic by default. Set `atomic = False` for operations that cannot run in a transaction. Keep runtime cursor/pooling choices separate. [Django migrations](https://docs.djangoproject.com/en/6.0/topics/migrations/) | Use one runner per database even when the role permits two connections. Do not use a wildcard source IP or a dynamic CI egress address; use a runner with stable public egress. Schema changes and logical backup/restore cannot be promised as one consistent point while your own DDL is in flight. To finish early, click **Revoke migration access** or run `dbhost databases migration-revoke --yes`. New logins stop and existing migration sessions are disconnected, and DBHost removes the matching network rules. At expiry, DBHost does the same on its own. A second request waits until that cleanup is finished, which usually takes under a minute. If the API returns `FEATURE_NOT_AVAILABLE`, the channel is not available for this database, typically because it has no managed SQL roles. Run migrations with the owner login through PgBouncer instead, and avoid tools that depend on session-level features. # Inactive databases Source: https://docs.dbhost.app/guides/inactive-databases When DBHost pauses an inactive Free or development database, what happens to your data, and how to start it again. DBHost runs every database on a shared PostgreSQL host. To keep room for active projects, Free and development databases that nobody uses are paused. ## When a database is paused A database is paused when all of these are true: * it is on the Free plan, or its environment is **development** * it has had no client connections for 14 days in a row * DBHost measured connections for the whole period without a gap * no backup, restore, role or connection-cap change is in progress Production databases on Pro or Business are never paused for inactivity. Connections from DBHost itself, such as backups, do not count as use, and missing measurements never count as inactivity. ## What happens to your data Nothing is deleted. Data, roles and credentials stay as they are. A paused database cannot change, so no new scheduled backups are taken and its existing backups are kept, also past the normal 30-day retention, until it runs again. The database status becomes `stopped`, and new connections are refused until you start it. The API shows why it is stopped: ```json theme={null} { "status": "stopped", "pause": { "state": "paused_inactive", "code": "DATABASE_PAUSED_INACTIVE", "inactivityDays": 14, "pausedAt": "2026-10-28T03:40:00.000Z", "message": "This database was paused after 14 days without connections. ...", "howToStart": { "dashboard": "/dashboard/", "api": { "method": "POST", "path": "/api/v1/databases//lifecycle", "body": { "action": "start" } } } } } ``` Requests that need a running database return `409 DATABASE_PAUSED_INACTIVE` with the same instructions. ## Start it again * Dashboard: open the database and select **Start** * API: `POST /api/v1/databases/{id}/lifecycle` with `{"action":"start"}` Starting checks capacity on the shared host first. If it is temporarily full, you get `503 CLUSTER_CAPACITY_FULL` and the database stays paused and unchanged; try again later. Any successful start, from the dashboard or the API, ends the pause. The 14-day count starts over from your next connection. # Next.js and Drizzle ORM with DBHost PostgreSQL Source: https://docs.dbhost.app/guides/nextjs-drizzle Connect a Next.js app to DBHost with Drizzle ORM: pooled runtime queries on port 6432 and Drizzle Kit migrations over the direct connection. If you want PostgreSQL in Next.js without a heavier ORM layer, Drizzle is the clean middle path: typed schema, typed queries, and SQL that still feels close to the database. This quickstart keeps to the shortest useful path: one DBHost database, one Drizzle setup, and one server-side query. **Best fit:** Teams that want type-safe SQL, explicit schema files, and a lighter abstraction than Prisma. **Not for:** Teams that want full ORM modeling, or teams that want zero schema tooling and prefer plain SQL only. If you want to compare this path with Prisma or plain `pg`, start with the [Next.js + PostgreSQL guide](/guides/nextjs-postgres). ## What you need * A DBHost account and one active database * A Next.js app * Node.js 18 or later * Drizzle ORM, `pg`, and Drizzle Kit installed in the app > [!NOTE] > > DBHost exposes PostgreSQL through PgBouncer on port `6432`. That pooled > connection string is the default path for normal Drizzle app queries. ## 1. Create a database in DBHost Create a database from the dashboard, then copy the connection string from the database detail page. If you are starting from zero, that is still the fastest first step. DBHost gives you the host, port, username, and password before you touch schema files or query code. ## 2. Install Drizzle From your Next.js project: ```bash theme={null} npm install drizzle-orm pg npm install -D drizzle-kit @types/pg ``` ## 3. Point Drizzle at DBHost Add the DBHost connection string to `.env.local` or the environment file your app already uses: ```bash theme={null} DATABASE_URL="postgresql://YOUR_USERNAME:PASSWORD@db.dbhost.app:6432/YOUR_DATABASE?sslmode=verify-full" ``` Use the exact database name and the selected runtime username from DBHost. Keep this private environment file out of source control. Managed runtime credentials do not own tables or run DDL. ## 4. Add the Drizzle config and schema ```ts theme={null} // drizzle.config.ts import "dotenv/config"; import { defineConfig } from "drizzle-kit"; export default defineConfig({ dialect: "postgresql", schema: "./app/lib/schema.ts", out: "./drizzle", dbCredentials: { // Drizzle Kit runs migrations, so use the direct migration profile, // not the pooled runtime URL. url: process.env.MIGRATION_DATABASE_URL!, }, }); ``` `MIGRATION_DATABASE_URL` is the [direct migration connection](/guides/direct-migrations) on `migrate.dbhost.app:5432`, available while a lease is active. Your app keeps using `DATABASE_URL`. ```ts theme={null} // app/lib/schema.ts import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"; export const projects = pgTable("projects", { id: serial("id").primaryKey(), name: text("name").notNull(), createdAt: timestamp("created_at", { withTimezone: true }) .defaultNow() .notNull(), }); ``` That is enough schema to prove the full path works. ## 5. Create the example table as the owner Create the matching `projects` table with your **owner** PostgreSQL login, for example through an owner SQL client: ```sql theme={null} CREATE TABLE IF NOT EXISTS projects ( id serial PRIMARY KEY, name text NOT NULL, created_at timestamptz NOT NULL DEFAULT now() ); ``` Or run `drizzle-kit migrate` with `MIGRATION_DATABASE_URL` set. Use the separate runtime credential for the app queries below. Do not treat the pooled runtime URL or role as a migration URL. ## 6. Create a reusable database client Use one shared `pg` pool and hand it to Drizzle: ```ts theme={null} // app/lib/db.ts import { drizzle } from "drizzle-orm/node-postgres"; import { Pool } from "pg"; import * as schema from "./schema"; const globalForDb = globalThis as unknown as { pool?: Pool; }; const pool = globalForDb.pool ?? new Pool({ connectionString: process.env.DATABASE_URL, }); if (process.env.NODE_ENV !== "production") { globalForDb.pool = pool; } export const db = drizzle(pool, { schema }); ``` This keeps development refreshes from creating a new connection pool every time the module reloads. ## 7. Query from a server component ```tsx theme={null} // app/page.tsx import { desc } from "drizzle-orm"; import { db } from "./lib/db"; import { projects } from "./lib/schema"; export default async function HomePage() { const rows = await db .select() .from(projects) .orderBy(desc(projects.createdAt)) .limit(5); return (

Projects

    {rows.map((project) => (
  • {project.name}
  • ))}
); } ``` That is enough to verify the database connection, Drizzle schema, and Next.js render path in one pass. ## When DBHost helps most * You want PostgreSQL working with Drizzle without managing PgBouncer yourself * You want one pooled URL ready on day one * You want backups and password resets from the dashboard later * You want the option to automate database actions on Pro or Business through the published CLI or REST API ## Next steps > **[Start for Free](https://dbhost.app/sign-up?utm_source=docs\&utm_medium=guide\&utm_campaign=nextjs_drizzle\&utm_content=start_for_free)** > > Create a DBHost account and provision your first database. > **[Compare Next.js Paths](/guides/nextjs-postgres)** > > See when Drizzle, Prisma, or plain `pg` is the better fit for your app. * See the [DBHost quickstart](/quickstart) for the shortest path to your first database. * See the [Next.js + PostgreSQL guide](/guides/nextjs-postgres) if you want to compare Drizzle with Prisma or plain `pg`. * See the [CLI](/cli) if you want to script database actions from your terminal. * See the [API reference](/api-reference/introduction) if your deployment pipeline already speaks HTTP. # Next.js and node-postgres (pg) with DBHost Source: https://docs.dbhost.app/guides/nextjs-pg Connect a Next.js app to DBHost with node-postgres: one shared pool on the pooled URL, verify-full TLS and a server-side query in a few steps. If you want the thinnest path from a Next.js app to PostgreSQL, use `pg`. You keep direct SQL, avoid ORM abstractions, and still get DBHost for pooling, backups, and day-to-day database management. This quickstart keeps to the shortest useful path: one DBHost database, one shared pool, and one server-side query. **Best fit:** Teams that want plain SQL, minimal dependencies, and full control over query text. **Not for:** Teams that want ORM models, generated clients, or schema tooling baked into the database layer. If you want to compare this path with Prisma or Drizzle, start with the [Next.js + PostgreSQL guide](/guides/nextjs-postgres). ## What you need * A DBHost account and one active database * A Next.js app * Node.js 18 or later * The `pg` package installed in the app > [!NOTE] > > Next.js Server Components run on the server, so your database credentials and > SQL stay out of the client bundle when you query from server-only code. ## 1. Create a database in DBHost Create a database from the dashboard, then copy the connection string from the database detail page. ## 2. Install `pg` From your Next.js project: ```bash theme={null} npm install pg npm install -D @types/pg ``` ## 3. Point the app at DBHost Add the DBHost connection string to `.env.local` or the environment file your app already uses: ```bash theme={null} DATABASE_URL="postgresql://YOUR_USERNAME:PASSWORD@db.dbhost.app:6432/YOUR_DATABASE?sslmode=verify-full" ``` Use the exact database name and the selected username from DBHost. On a managed database, use the runtime role for app queries and the owner role for the schema step below. Keep the private environment file out of source control. ## 4. Create a simple table With plain `pg`, you own the schema workflow. For a quickstart, create one table with the **owner** login through a PostgreSQL SQL client. The runtime role on a managed database cannot run DDL: ```sql theme={null} CREATE TABLE IF NOT EXISTS projects ( id bigserial PRIMARY KEY, name text NOT NULL, created_at timestamptz NOT NULL DEFAULT now() ); ``` That gives the app something simple to query immediately. ## 5. Create one shared pool Keep the pool in one reusable server-only file: ```ts theme={null} // app/lib/db.ts import { Pool } from "pg"; const globalForPg = globalThis as unknown as { pool?: Pool; }; export const pool = globalForPg.pool ?? new Pool({ connectionString: process.env.DATABASE_URL, }); if (process.env.NODE_ENV !== "production") { globalForPg.pool = pool; } ``` That keeps development reloads from creating a new pool every time the file changes. ## 6. Query from a server component ```tsx theme={null} // app/page.tsx import { pool } from "./lib/db"; export default async function HomePage() { const result = await pool.query( ` SELECT id, name, created_at FROM projects ORDER BY created_at DESC LIMIT 5 `, ); return (

Projects

    {result.rows.map((project) => (
  • {project.name}
  • ))}
); } ``` That is enough to verify the database connection, pooled query path, and Next.js render path in one pass. ## When DBHost helps most * You want a plain PostgreSQL client without running your own pooler * You want backups and password resets from the dashboard later * You want the option to automate database actions on Pro or Business through the published CLI or REST API * You want PostgreSQL without turning database operations into a platform project ## Next steps > **[Start for Free](https://dbhost.app/sign-up?utm_source=docs\&utm_medium=guide\&utm_campaign=nextjs_pg\&utm_content=start_for_free)** > > Create a DBHost account and provision your first database. > **[Compare Next.js Paths](/guides/nextjs-postgres)** > > See when plain `pg`, Drizzle, or Prisma is the better fit for your app. * See the [DBHost quickstart](/quickstart) for the shortest path to your first database. * See the [Next.js + PostgreSQL guide](/guides/nextjs-postgres) if you want to compare plain `pg` with Drizzle or Prisma. * See the [CLI](/cli) if you want to script database actions from your terminal. * See the [API reference](/api-reference/introduction) if your deployment pipeline already speaks HTTP. # Next.js and PostgreSQL: Prisma, Drizzle or pg Source: https://docs.dbhost.app/guides/nextjs-postgres Choose Prisma, Drizzle or node-postgres for a Next.js app on DBHost: pooled runtime queries on port 6432 and a direct connection for migrations. If you want PostgreSQL in a Next.js app on DBHost, the first decision is not the host. It is the data layer. The shortest useful split is: * Prisma if you want a full ORM and the fastest path to application models * Drizzle if you want SQL-shaped TypeScript with less abstraction * `pg` if you want a plain PostgreSQL client and no ORM at all DBHost fits all three the same way: create a database, copy the pooled connection string, keep queries on the server, and let DBHost handle pooling, backups, and credentials. ## Which path fits your app | Path | Best when | What you get | Tradeoff | | - | - | - | - | | Prisma | You want an ORM for runtime queries | Schema modeling and a generated client | Prisma Migrate needs the direct migration URL | | Drizzle | You want typed SQL and lighter tooling | SQL-first schema and typed queries | Drizzle Kit should use the direct migration URL | | `pg` | You want the thinnest possible layer | Direct SQL, minimal dependencies, full control | You own schema and query ergonomics yourself | ## Recommended default `pg` is the most direct end-to-end path: it works with the pooled runtime connection, and an owner SQL client can create the example table. Prisma Client and Drizzle use the same pooled connection for app queries; run their schema tools over the [direct migration connection](/guides/direct-migrations). Drizzle is the clearest alternative for teams that want SQL ownership without dropping type safety. Plain `pg` suits teams that do not want an ORM, and also cron jobs, route handlers, and internal tools that just need SQL. ## DBHost-specific fit All three paths use the same DBHost building blocks: * one PostgreSQL database * one pooled connection string on port `6432` with [verify-full TLS](/guides/tls-connections) * separate [owner and runtime roles](/guides/database-roles) when you choose managed SQL roles * one place to manage passwords and backups * one dashboard on every plan, with CLI and API automation on Pro and Business The library changes. The platform path stays the same. ## Guides > **[Prisma](/guides/nextjs-prisma)** > > Use Prisma Client for runtime queries after the schema is created through an approved owner path. > **[Drizzle](/guides/nextjs-drizzle)** > > Use Drizzle when you want a lighter SQL-first TypeScript workflow. > **[pg](/guides/nextjs-pg)** > > Use node-postgres when you want direct SQL without an ORM layer. ## Next steps * Start with the guide that matches your current stack preferences. * Keep database queries in Server Components, route handlers, or server actions. * Use a pooled URL for app queries. Keep schema-changing credentials and migration tooling separate from the runtime role, and run migrations over the [direct migration connection](/guides/direct-migrations). # Next.js and Prisma with DBHost PostgreSQL Source: https://docs.dbhost.app/guides/nextjs-prisma Connect Prisma Client to a pooled DBHost database from Next.js and run Prisma Migrate over the direct migration connection with the owner role. DBHost serves application queries through PgBouncer in transaction mode on port `6432`. Prisma Client can use a pooled runtime URL through its PostgreSQL driver adapter. **Prisma Migrate and `prisma db push` need a direct database connection** according to [Prisma's PgBouncer guide](https://www.prisma.io/docs/orm/v7/prisma-client/setup-and-configuration/databases-connections/pgbouncer). On DBHost, use the [direct migration connection](/guides/direct-migrations) (available for databases with managed SQL roles) for those commands, and the pooled URL for your app. ## 1. Create a database and choose a role Create a database from the DBHost dashboard. **Managed SQL roles** gives you separate owner, runtime, and read-only credentials on every plan. Use the runtime role for your app and the owner role for schema changes. With legacy creation, the single database login remains the owner. Copy the exact database name, selected username, and password from the detail page. A managed username is generated separately from the database name. Keep the connection URL in a private environment store and never commit it. ## 2. Configure Prisma Client This example targets Prisma ORM 7, which requires Node.js 20.19 or later. Pin the packages to one major version: ```bash theme={null} npm install @prisma/client@7 @prisma/adapter-pg@7 pg npm install -D prisma@7 ``` Set the pooled runtime URL in your app's private environment. Replace every placeholder with the values for the **runtime** role (or the legacy owner): ```bash theme={null} DATABASE_URL="postgresql://YOUR_USERNAME:PASSWORD@db.dbhost.app:6432/YOUR_DATABASE?sslmode=verify-full" ``` Generate Prisma Client from your own schema using an explicit output path as shown in [Prisma's generator guide](https://www.prisma.io/docs/orm/v7/prisma-client/setup-and-configuration/generating-prisma-client). Then pass a PostgreSQL adapter the pooled URL in server-only code: ```ts theme={null} // app/lib/prisma.ts import { PrismaPg } from "@prisma/adapter-pg"; import { PrismaClient } from "../generated/prisma/client"; const connectionString = process.env.DATABASE_URL; if (!connectionString) throw new Error("DATABASE_URL is required"); const adapter = new PrismaPg({ connectionString }); export const prisma = new PrismaClient({ adapter }); ``` Adjust the import to the output path you chose in `schema.prisma`. Keep this module on the server; do not import it into a Client Component. ## Schema changes Do not point `prisma migrate dev`, `prisma migrate deploy`, or `prisma db push` at the PgBouncer runtime URL. Prisma reads a separate migration URL from `prisma.config.ts`. Set it to the direct migration profile while a lease is active: ```ts theme={null} // prisma.config.ts import "dotenv/config"; import { defineConfig, env } from "prisma/config"; export default defineConfig({ schema: "prisma/schema.prisma", datasource: { // postgresql://MIGRATION_USER:PASSWORD@migrate.dbhost.app:5432/YOUR_DATABASE?sslmode=verify-full url: env("MIGRATION_DATABASE_URL"), }, }); ``` Your app keeps using `DATABASE_URL` on port 6432. On a single-login database without managed SQL roles, apply reviewed SQL with the owner login instead. A runtime role cannot perform DDL. `prisma migrate dev` can also require a separate shadow database; DBHost does not grant `CREATEDB` on the shared PostgreSQL host. See [Prisma's shadow database guide](https://www.prisma.io/docs/orm/prisma-migrate/understanding-prisma-migrate/shadow-database) before using that command. ## Next steps * [DBHost quickstart](/quickstart) for connection details and TLS. * [Next.js + pg](/guides/nextjs-pg) for an end-to-end query with no ORM migration dependency. * [Prisma's PgBouncer guidance](https://www.prisma.io/docs/orm/v7/prisma-client/setup-and-configuration/databases-connections/pgbouncer) for current adapter and direct-URL requirements. # Supported PostgreSQL extensions: citext, pg_trgm, pgcrypto Source: https://docs.dbhost.app/guides/postgres-extensions DBHost supports three PostgreSQL extensions: citext, pg_trgm and pgcrypto. Install or upgrade them from the dashboard, API or CLI. DBHost supports exactly three PostgreSQL 16 extensions: | Extension | Typical use | | - | - | | `citext` | Case-insensitive text, for example email addresses | | `pg_trgm` | Trigram similarity and fast `LIKE` / `ILIKE` search with GIN or GiST indexes | | `pgcrypto` | Hashing, random UUIDs and symmetric encryption functions | Other extensions are not offered, and you cannot install arbitrary extensions with `CREATE EXTENSION`. ## Requirements * The database uses [managed SQL roles](/guides/database-roles). Install and upgrade are not available on single-login databases. * The database owner performs the change. * API and CLI access require Pro or Business. The dashboard works on every plan. ## Install or upgrade **Dashboard:** open the database page and find **PostgreSQL extension catalog**. It lists each extension with its available version, the version installed in this database, and a DBHost status. Use the action next to an extension to install or upgrade it. **API:** read the inventory with `GET /api/v1/databases/{id}/extensions`, then submit one operation: ```bash theme={null} curl -X POST https://dbhost.app/api/v1/databases//extensions/operations \ -H "Authorization: Bearer $DBHOST_API_KEY" \ -H "Idempotency-Key: install-citext-1" \ -H "Content-Type: application/json" \ -d '{"extension":"citext","kind":"install"}' ``` `kind` is `install` or `upgrade`. Poll `GET /api/v1/databases/{id}/extensions/operations/{operationId}` until it finishes. If a response is uncertain, retry with the same idempotency key; a new key requests a new operation. **CLI:** ```bash theme={null} dbhost databases extensions dbhost databases extensions install citext --idempotency-key install-citext-1 dbhost databases extensions status ``` `pgcrypto` through the CLI requires CLI 0.3.7 or newer. ## After installing Extensions are installed into the database, so your migrations can use them directly, for example `email citext NOT NULL` or `CREATE INDEX ... USING gin (name gin_trgm_ops)`. DBHost logical backups include the extension. # Preview databases for CI and pull requests Source: https://docs.dbhost.app/guides/preview-databases Create short-lived DBHost preview databases from CI with a 24-hour lease, renewals up to 72 hours, optional daily backup and 7-day recovery. Preview databases give each pull request or CI run its own PostgreSQL database that expires on its own. Use them for synthetic test data only; do not put production or personal data in a preview. > [!NOTE] > > Preview databases are available on Pro and Business. ## CI key Create a **preview CI key** under **Settings** → **API keys**. Each key starts with `dbhci_` and is limited to one namespace, for example your repository. A normal `dbh_` API key cannot manage previews, and a CI key cannot manage other databases. ## Lease | Setting | Value | | - | - | | Default lease | 24 hours | | Each renewal | Up to 72 hours | | Quota | Counts against your plan's database limit | Create a preview with `POST /api/v1/previews` and a `ref` such as `pull/42`. Repeating the same ref with the same choices returns the same preview, so a re-run CI job does not create a second database. Renew with `POST /api/v1/previews/{databaseId}/extend`. An expired lease cannot be renewed. ```bash theme={null} curl -X POST https://dbhost.app/api/v1/previews \ -H "Authorization: Bearer $DBHOST_PREVIEW_CI_KEY" \ -H "Content-Type: application/json" \ -d '{"ref":"pull/42","ttlHours":24,"scheduledBackup":"none"}' ``` ## Backup choice | Choice | Behavior | | - | - | | `none` (default) | No daily logical backup | | `daily` | Daily logical backup, kept 30 days | Change it while the preview is active with `PATCH /api/v1/previews/{databaseId}/backup-policy`. Infrastructure snapshots of the host can still contain preview data; see [Backups and retention](/backups-and-retention). ## Delete and recover Delete a preview with `DELETE /api/v1/previews/{databaseId}`, or let the lease expire. DBHost takes a verified deletion backup, and the preview can be recovered for 7 days. Recovery needs the account owner and a full-account key (`POST /api/v1/databases/{id}/recover`); the CI key cannot recover. A recovered preview starts a new 24-hour lease. After 7 days the preview and its deletion backup are deleted permanently. Full request and response details are in the [Preview CI lifecycle reference](/api-reference/previews/ci-lifecycle). # Restore a database from a DBHost backup Source: https://docs.dbhost.app/guides/restore-from-backup Restore a DBHost database in place from one of its own verified backups, from the dashboard or the API, and what to expect during the cutover. You can restore a database in place from a backup that DBHost made of **that same database**. The restore replaces the current contents, so any writes made after the backup was taken are gone afterwards. To load a dump from somewhere else, see [Restore your own dump](/guides/restore-your-own-dump) instead. > [!NOTE] > > In-place restore is available for every database. To load a dump from > somewhere else, download the backup and > [restore your own dump](/guides/restore-your-own-dump). ## Before you start * Only the database owner (or an admin, in the dashboard) can restore. * Pick a backup that is listed for the database and is less than 30 days old; see [Backups and retention](/backups-and-retention). * Expect a short cutover. Open connections are dropped and your app must reconnect. Plan a quiet moment or a maintenance window. DBHost restores into an isolated copy first, verifies it, and takes a fresh backup of the current database before it switches over. If verification fails, the current database is left unchanged. ## From the dashboard 1. Open the database and go to **Backups**. 2. Choose **Restore** on a verified backup. 3. Confirm. The page shows progress until the operation finishes. The dashboard shows **Restore** when it can restore that database, which includes databases with managed SQL roles. If the button is not shown, use the API flow below. ## With the API API access requires Pro or Business. The additional-roles and managed SQL roles routes also need a full-account API key; a key scoped to selected databases gets `403 KEY_SCOPE_DENIED`. List backups first to find the `filename` and, where present, the backup `id`: ```bash theme={null} curl -H "Authorization: Bearer $DBHOST_API_KEY" \ https://dbhost.app/api/v1/databases//backups ``` Then use the route that matches the database: | Database | Route | | - | - | | Single login, no additional roles | [`POST /api/v1/databases/{id}/backups/{filename}`](/api-reference/backups/restore) | | Single login with additional roles | `POST /api/v1/databases/{id}/legacy-overlay-restores`; see [Restore a v4 backup](/api-reference/databases/additional-roles#restore-a-v4-backup) | | Managed SQL roles | `POST /api/v1/databases/{id}/managed-restores` with `{"backupId":"..."}` and an `Idempotency-Key`, then `POST /api/v1/operations/{operationId}/managed-restore/advance` until it succeeds | Send an `Idempotency-Key` header with a value you generate and keep, on every route, including the first request: ```bash theme={null} curl -X POST \ -H "Authorization: Bearer $DBHOST_API_KEY" \ -H "Idempotency-Key: restore-2026-10-14-orders" \ https://dbhost.app/api/v1/databases//backups/ ``` Every route returns an operation. Poll [`GET /api/v1/operations/{id}`](/api-reference/operations/get) until its status is `succeeded` or `failed`. If a request times out, retry with the same `Idempotency-Key` to get the original operation back. Without it, a retry can start a second restore that overwrites writes made after the first one. The CLI has no restore command. ## After the restore * Check that your application reconnects and its queries work. * Logins and passwords stay the same; you do not need a new connection string. * A database that has additional roles can only be restored from a backup taken after the opt-in. Older backups do not contain those roles. ## Limits * No point-in-time recovery: you can restore to a backup, not to an arbitrary moment. * You cannot restore one database's backup into a different database. * Cloning a database that has additional roles returns `409 LEGACY_OVERLAY_REQUIRES_V4`. # Import your own SQL dump with psql or pg_restore Source: https://docs.dbhost.app/guides/restore-your-own-dump DBHost does not accept uploaded dumps. Import .sql, .sql.gz or custom-format dumps yourself with psql or pg_restore using the owner role over TLS. DBHost does not restore files that you upload. The dashboard and API reject customer-uploaded `.sql`, `.sql.gz`, `.dump` and `.backup` files with `409 CUSTOM_RESTORE_UPLOAD_DISABLED`; see [Customer restore upload unavailable](/api-reference/backups/restore-upload). Instead, run the import yourself with standard PostgreSQL tools. ## 1. Prepare * Use the **owner** role's credentials from the database page. On a single-login database, use that login. Runtime and read-only roles cannot create tables. * Take an on-demand backup of the target first, so you can go back. * Review the dump. It runs with your owner's privileges, and anything that needs superuser (for example `CREATE ROLE`, `ALTER SYSTEM` or unsupported extensions) will fail. * Use the [TLS settings](/guides/tls-connections) for your client; use `sslmode=verify-full`. For large imports, consider the [direct migration connection](/guides/direct-migrations). It avoids transaction pooling and has longer timeouts than the [query timeout profiles](/guides/connection-limits-and-timeouts). ## 2. Import Plain SQL (`.sql`): ```bash theme={null} PGSSLMODE=verify-full PGSSLROOTCERT=system \ psql -h db.dbhost.app -p 6432 -U YOUR_OWNER_ROLE -d YOUR_DATABASE \ -v ON_ERROR_STOP=1 -f backup.sql ``` Compressed SQL (`.sql.gz`): decompress first, or stream it: ```bash theme={null} gunzip -c backup.sql.gz | PGSSLMODE=verify-full PGSSLROOTCERT=system \ psql -h db.dbhost.app -p 6432 -U YOUR_OWNER_ROLE -d YOUR_DATABASE -v ON_ERROR_STOP=1 ``` Custom-format dumps (`.dump`, or `.backup` in PostgreSQL custom format): ```bash theme={null} PGSSLMODE=verify-full PGSSLROOTCERT=system \ pg_restore --no-owner --no-privileges --exit-on-error \ -h db.dbhost.app -p 6432 -U YOUR_OWNER_ROLE -d YOUR_DATABASE backup.dump ``` `--no-owner` makes your owner role own every restored object, and `--no-privileges` skips grants to roles that do not exist on DBHost. `PGSSLROOTCERT=system` needs libpq 16 or newer; with an older client, point it at your system CA bundle. When you create the dump on the source server, `pg_dump --no-owner --no-acl` avoids most ownership and grant errors up front. ## 3. Fix grants and ownership * Grant your runtime, read-only and [additional roles](/guides/database-roles) access to the imported tables if they need it. * If your app gets `permission denied for table ...` after an import, run [repair ownership](/api-reference/databases/repair-ownership) or `dbhost databases repair-ownership ` to give the owner role ownership of every table, sequence and view. ## Restoring a DBHost backup instead If the dump is a backup that DBHost made of the same database, use [Restore from a DBHost backup](/guides/restore-from-backup); DBHost runs and verifies that restore for you. # Self-Hosted Postgres vs DBHost for Small SaaS Teams Source: https://docs.dbhost.app/guides/self-hosted-postgres-vs-dbhost Compare self-hosted PostgreSQL and DBHost on what small SaaS teams care about: pooling, backups, automation, limits and the time you spend on ops. Small SaaS teams usually do not choose PostgreSQL in the abstract. They choose between spending time on product work or spending time on database operations. That tradeoff is what this guide is about. **Best fit:** Small SaaS teams deciding whether database operations should stay on the product roadmap or move into managed infrastructure now. **Not for:** Teams that already need host-level control, custom networking, or a dedicated DBA workflow. ## The short version Choose self-hosted PostgreSQL if you already want to own the server, the patching cycle, the backup routine, the connection pool, and the restore drills. Choose DBHost if you want PostgreSQL to feel like a product surface instead of an infrastructure project. ## What DBHost ships DBHost gives you the pieces most small teams end up building themselves later: * A PostgreSQL database you can create from the dashboard * A pooled connection through PgBouncer on every database * Daily automated backups for standard databases and on-demand backup triggers * A REST API for database operations on Pro and Business * A published CLI for paid-plan terminal and CI workflows * Scoped API keys on paid plans for least-privilege automation * A dashboard for credentials and database management ## Comparison | Need | Self-hosted PostgreSQL | DBHost | | - | - | - | | Provision a new database | You set up the server and create the database yourself | Create it in the dashboard; Pro and Business can also use CLI or API | | Handle pooling | You configure PgBouncer or another pooler | Every database gets PgBouncer support | | Handle backups | You design and test the backup job | Standard databases receive daily automated backups | | Trigger backups on demand | You script it | Use the dashboard on every plan, or the API/CLI on paid plans | | Manage credentials | You build the process | Use scoped API keys and dashboard password resets | | Automate repeatable ops | You build the tooling | Use the CLI or REST API on Pro and Business | | Support future app growth | You keep extending your own ops stack | You keep the same control plane as usage grows | ## When self-hosted still makes sense * You need full control of the host, network, and OS * You already have a strong DBA or platform team * Your compliance or networking model needs a custom setup * You are comfortable owning backup testing and restore procedures yourself Those are valid reasons. Self-hosting is not wrong. It just moves the operational burden onto your team. ## When DBHost is a better fit * You want to launch without standing up database infrastructure first * You want pooling and backups without designing them from scratch * You want programmatic database operations but do not want to maintain a custom admin surface * You want a small team to stay focused on shipping the app, not the database platform For many SaaS teams, that is the real requirement: not more control, but less overhead. ## A practical decision rule If database operations are already part of your team’s job, self-hosting can be fine. If database operations are becoming accidental work, DBHost is usually the cleaner path. ## Where DBHost helps in practice DBHost is especially useful when you want: * A database for a Next.js app, service, or internal tool * A pooled PostgreSQL endpoint without managing PgBouncer yourself * A CLI and REST API on Pro and Business for repeatable operations * A dashboard for day-to-day database management * Scoped API keys for scripts, CI, and limited integrations ## Next step > **[Pricing](https://dbhost.app/pricing?utm_source=docs\&utm_medium=guide\&utm_campaign=self_hosted_vs_dbhost\&utm_content=pricing)** > > Compare Free, Pro, and Business based on the workflow you need today. > **[Quickstart](/quickstart)** > > Create one non-critical database and compare the day-one workload yourself. If you want to judge the fit quickly, create one non-critical database and compare the day-one workload against your current setup. Start with the [quickstart](/quickstart), then check the [CLI](/cli) and [API reference](/api-reference/introduction) if you want to automate the same workflows. # Connect to DBHost with verify-full TLS Source: https://docs.dbhost.app/guides/tls-connections DBHost requires TLS; use sslmode=verify-full. Set up certificate verification for psql, node-postgres, Prisma, Drizzle and psycopg with a public CA. DBHost only accepts encrypted (TLS) connections. Certificate verification happens in your client, so set `sslmode=verify-full`: the client then checks that the server certificate is signed by a trusted CA and matches the host name. Weaker modes such as `sslmode=require` encrypt the connection but do not verify the server, so do not use them. | Endpoint | Use | | - | - | | `db.dbhost.app:6432` | Application traffic through PgBouncer | | `migrate.dbhost.app:5432` | Short-lived [direct migration access](/guides/direct-migrations) | The certificate is issued by Let's Encrypt, a public CA. You do not need to download a DBHost-specific CA file. Point the client at the operating system's trusted CA store instead. ## psql and libpq libpq 16 or newer accepts `sslrootcert=system`, which uses the system CA store: ```bash theme={null} psql -W "host=db.dbhost.app port=6432 dbname=YOUR_DATABASE user=YOUR_USERNAME sslmode=verify-full sslrootcert=system" ``` With an older libpq, set `sslrootcert` to your system CA bundle, for example `/etc/ssl/certs/ca-certificates.crt` on Debian and Ubuntu or `/etc/ssl/cert.pem` on macOS. `pg_dump` and `pg_restore` take the same connection parameters. ## Node.js: pg, Prisma and Drizzle node-postgres verifies the server against Node's built-in CA list. Keep `sslmode=verify-full` in the connection string: ```bash theme={null} DATABASE_URL="postgresql://YOUR_USERNAME:PASSWORD@db.dbhost.app:6432/YOUR_DATABASE?sslmode=verify-full" ``` ```ts theme={null} import { Pool } from "pg"; export const pool = new Pool({ connectionString: process.env.DATABASE_URL }); ``` Prisma with `@prisma/adapter-pg` and Drizzle with `drizzle-orm/node-postgres` both use `pg` underneath, so the same URL works. Do not add `rejectUnauthorized: false`; that turns verification off. ## Python: psycopg psycopg 3 uses libpq. With libpq 16 or newer: ```python theme={null} import os import psycopg conn = psycopg.connect( os.environ["DATABASE_URL"], sslmode="verify-full", sslrootcert="system", ) ``` ## Troubleshooting * `certificate verify failed` or `unable to get local issuer certificate`: the client cannot find a trusted CA store. Use `sslrootcert=system` with libpq 16+, or point `sslrootcert` at the system CA bundle. * `server certificate does not match host name`: connect with the exact host name shown in the dashboard, not an IP address. * `sslrootcert=system` rejected: your libpq is older than 16. Use a CA bundle path instead. See [Quickstart](/quickstart) for connection details and [Database roles](/guides/database-roles) to choose which login to use. # Introduction Source: https://docs.dbhost.app/introduction DBHost is managed PostgreSQL hosting with PgBouncer pooling, daily backups, separate SQL roles, scoped API keys and a read-only SQL explorer. DBHost is a managed PostgreSQL hosting service. Sign up, create a database, and get a connection string — we handle the server, connection pooling, and backups. No infrastructure to manage. ## What you get * **Web dashboard** at [dbhost.app](https://dbhost.app) for creating, managing, and monitoring databases * **Database lifecycle controls** for starting, stopping, restarting, and resetting credentials * **Role-based shared access** for collaborating without sharing one password everywhere * **Per-database IP allowlists** enforced at the PgBouncer layer * **SQL Explorer** for owners to browse public tables and run guarded read-only SQL in the dashboard * **REST API** on Pro and Business for programmatic access and CI/CD integration * **CLI** on Pro and Business, published as `@dbhost-app/cli`, for terminal-based workflows * **Scoped API keys** on paid plans for least-privilege third-party integrations * **Encrypted stored database credentials** so tenant passwords are protected at rest in the control plane * **PgBouncer** connection pooling on every database * **Daily automated backups** for production databases (and by choice for others) with `pg_dump`, kept 30 days, plus on-demand backups on every plan; see [Backups and retention](/backups-and-retention) ## How it works DBHost has two parts: * **Control plane** — the dashboard, REST API, and CLI. It handles sign-in, API keys, team access, and database metadata. * **Data plane** — PostgreSQL 16 behind PgBouncer. Your application connects to `db.dbhost.app:6432` over TLS; schema migration tools can request a short-lived direct connection on `migrate.dbhost.app:5432`. Customer-facing operations stay tenant-scoped. The in-dashboard explorer is owner-only and read-only. ## Next steps > **[Quickstart](/quickstart)** > > Create your first database in under 2 minutes. > **[API reference](/api-reference/introduction)** > > Integrate DBHost into your workflow. > **[Dashboard guide](/dashboard/databases)** > > Learn the end-user dashboard, backups, explorer, and account workflows. > **[Concepts](/concepts)** > > Understand the public product model and core building blocks. # Limits and what DBHost does not offer Source: https://docs.dbhost.app/limits Plan quotas, per-database connection and role limits, the shared host model, and features DBHost does not offer, such as PITR, replicas or an SLA. DBHost is built for small apps, internal tools and staging environments. This page lists the limits that matter when you decide whether it fits. ## Plans | Plan | Databases | Storage per database | Server connection cap (default / max) | API and CLI | | - | - | - | - | - | | Free | 1 | 250 MB | 2 / 4 | No | | Pro | 5 | 1 GB | 3 / 12 | Yes | | Business | 20 | 2 GB | 4 / 24 | Yes | Prices and billing are on [Settings](/dashboard/settings#billing). [Preview databases](/guides/preview-databases) count toward the database limit and are available on Pro and Business with limited access. ## Per database | Limit | Value | | - | - | | Server connection cap | Plan default (2 for development databases), adjustable from 2 to the plan maximum, shared by all roles ([details](/guides/connection-limits-and-timeouts)) | | Query timeout profiles | Standard 60/10/30 s, Balanced 30/3/15 s, Short 15/1/5 s (statement / lock / idle in transaction) | | SQL roles | Owner, runtime and read-only, plus up to 10 additional roles ([details](/guides/database-roles)) | | Extensions | `citext`, `pg_trgm` and `pgcrypto` only ([details](/guides/postgres-extensions)) | | Direct migration access | Exact public IPs only, lease up to 120 minutes, 1–2 sessions ([details](/guides/direct-migrations)) | | Logical backup retention | 30 days ([details](/backups-and-retention)) | | Deleted database recovery | 7 days, then permanent deletion | ## Shared host Customer databases run on a shared PostgreSQL 16 host. SQL roles, grants and the connection cap separate access, but CPU, memory and disk are shared. There is no hard per-database CPU, memory or disk quota, and a busy neighbor can affect latency. New databases and connection cap increases are admitted from measured connection use on the host. When the host is close to full, DBHost refuses the request with `503 CLUSTER_CAPACITY_FULL` before the database is created; retry later with the same request. ## Inactive databases Free and development databases that have had no client connections for 14 days in a row are paused. A paused database keeps its data, roles, credentials and existing backups. Its status is `stopped` and the API returns a `pause` object. Start it from the dashboard or the API; see [Inactive databases](/guides/inactive-databases). ## Not offered * Point-in-time recovery (PITR) * High availability, failover or read replicas * A service level agreement (SLA) * Restoring uploaded SQL files or dumps through DBHost; import them yourself with [psql or pg\_restore](/guides/restore-your-own-dump) * Cloning a database that has additional roles * Extensions other than `citext`, `pg_trgm` and `pgcrypto` * Superuser, `CREATEDB` or `CREATEROLE` for customer roles * Multiple regions or region choice # DBHost quickstart: your first PostgreSQL database Source: https://docs.dbhost.app/quickstart Create a managed PostgreSQL database on DBHost in about two minutes, pick your SQL roles and connect with psql over verify-full TLS on port 6432. **1. Sign up** Go to [dbhost.app](https://dbhost.app) and create an account with email or Google. **2. Create a database** Click **+ New Database** in the dashboard and enter a display name. Check **Managed SQL roles** if you want separate owner, runtime, and read-only logins. It is available on every plan; leaving it unchecked uses the existing single-login creation flow. DBHost generates the internal PostgreSQL identifier automatically, provisions the database in seconds, and shows the connection details immediately. **3. Connect** Use the connection string from the database detail page. It connects through PgBouncer on port 6432: ```bash theme={null} psql -W "host=db.dbhost.app port=6432 dbname=YOUR_DATABASE user=YOUR_USERNAME sslmode=verify-full sslrootcert=system" ``` Use the database name and the specific role username shown on the detail page. `-W` prompts for the password so it does not appear in your shell command. Other PostgreSQL clients, ORMs, and drivers use the same host, port, and selected role credentials. `sslrootcert=system` needs libpq 16 or newer; see [TLS connections](/guides/tls-connections) for older clients and other drivers. ## Connection details Each database comes with: | Field | Description | | - | - | | **Host** | `db.dbhost.app` | | **Port** | 6432 (PgBouncer) | | **Database** | Auto-generated internal database name | | **Username** | Legacy: same as the internal database name. Managed: a generated username for each owner, runtime, and read-only role. | | **Password** | Auto-generated for each login; reveal or rotate only the credential you need. | > [!WARNING] > > Keep revealed passwords in a secret store and out of shell history and source > control. The database detail page can reveal or rotate an authorized role's > current credential; legacy databases also support password reset. ## Prefer the CLI? Pro and Business users can work from the terminal with the [CLI](/cli): ```bash theme={null} npx @dbhost-app/cli@latest --help ``` Then authenticate with an API key from **Settings**: ```bash theme={null} dbhost auth login --api-key dbh_your_api_key_here ``` For paid plans, `Selected databases` is the recommended key scope. Use `Full account access` only if your automation needs to create databases. ## What happens under the hood When you create a database, DBHost: 1. Creates the legacy owner login, or a managed owner/runtime/read-only role set when you opt in, with separate generated credentials 2. Creates a database owned by the selected owner login 3. Adds a PgBouncer pool entry for connection multiplexing 4. Reloads PgBouncer (zero-downtime config reload via SIGHUP) 5. Applies the backup choice for its environment: production always gets a daily logical backup kept for 30 days; see [Backups and retention](/backups-and-retention) 6. Records the action in the platform activity log # DBHost security model and honest limits Source: https://docs.dbhost.app/security How DBHost protects your PostgreSQL data: verify-full TLS, scoped API keys, IP allowlists, encrypted credentials, backups and what it does not offer. DBHost is built for developers who want safer defaults without turning Postgres into its own side project. For the shorter product-facing overview, see the [public security page](https://dbhost.app/security). ## Current security model DBHost separates the control plane from the data plane: * The **control plane** handles auth, dashboard actions, API keys, and metadata * The **data plane** runs PostgreSQL, PgBouncer, backups, and the internal agent that performs database operations Customer-facing operations stay tenant-scoped. Direct PostgreSQL access still uses the database credentials shown on the database detail page, while the dashboard explorer and public API stay behind tighter product boundaries. ## Access and authentication * Dashboard access uses Clerk authentication * REST API access on Pro and Business uses DBHost API keys * API keys are scoped either to the full account or to selected databases * Selected-database keys cannot create databases or operate outside their assigned scope * The internal agent is not a customer auth surface; it is called by the control plane with separate internal credentials For user-facing setup details, see [API authentication](/api-reference/authentication) and [API keys](/dashboard/api-keys). ## Guarded SQL exploration The in-dashboard SQL Explorer is intentionally tighter than a normal database client: * owner-only * read-only * limited to the `public` schema * executed through a dedicated explorer role * validated before queries run This keeps the explorer useful for inspection and one-off reads without turning it into a broad write surface. ## Abuse resistance DBHost now adds basic server-side abuse controls for `curl`, scripts, bots, and direct HTTP clients: * request-size limits on public API routes * rate limiting on public API routes * auth-failure and abuse logging at the edge * host firewall rules around the data plane These controls are designed to slow probing and noisy abuse without changing the public API contract. ### Temporary blocks after repeated failed logins If one IP address sends 30 or more failed logins to a database connection (port `6432`) within 10 minutes, DBHost blocks that address on port `6432` only. Nothing else is affected: the dashboard, the API and every other port stay reachable from that address. * The first block lasts 5 minutes and lifts by itself. A repeat block within a day lasts 15 minutes, which is the longest block there is. * Common cause: you rotated a database password, and an app instance or retry loop still uses the old one. Update every client to the new password. Connections work again as soon as the block lifts. * Addresses shared by many apps (serverless platforms such as Vercel) can be blocked by failures from another app on the same address. The block is short, and it clears without any action from you. * Successful logins are never counted, and a block never removes data or changes your database. > [!NOTE] > > CORS is not treated as a security boundary. Browser origin checks help with > browser behavior, but direct HTTP clients still need proper authentication and > server-side controls. ## Operational safeguards * every application connection goes through PgBouncer on port `6432` with required TLS (connect with `sslmode=verify-full`); the only direct path is a short-lived, IP-restricted migration lease on `migrate.dbhost.app:5432` * per-database IP allowlists are enforced at the pool layer * SQL roles and a per-database PgBouncer server cap limit access and pooled connections; shared CPU, memory, and disk are not hard-isolated per customer * production databases always receive daily logical backups kept for 30 days; staging, development, other and preview databases follow their backup choice ([details](/backups-and-retention)) * on-demand backups can be triggered before risky changes * tenant database passwords are encrypted at rest in the control plane * destructive operations require a managed-registry binding, a matching operation ID and database ID/name binding, an explicit capability, and a target outside the permanent protected list * `dbhost`, `postgres`, `template0`, `template1`, and the agent auth/registry databases are permanently protected from drop, rename, restore, swap, and purge operations; there is no override The repository's destructive test coverage uses isolated local containers with synthetic data. Production hosts and addresses are denied test targets. ## Honest limits DBHost is a strong fit for: * small SaaS apps * internal tools * staging environments * agencies managing several app databases DBHost does not offer: * point-in-time recovery (PITR) * high availability, failover or read replicas * a service level agreement (SLA) * multi-region hosting * restore of uploaded dumps; you import them yourself with `psql` or `pg_restore` * cloning of databases that have additional roles * extensions other than `citext`, `pg_trgm` and `pgcrypto` * a compliance certification See [Limits](/limits) for plan and per-database limits. That matters because security claims are only useful when they match the actual product surface. ## Next steps * Read the [public security overview](https://dbhost.app/security) * Review [API authentication](/api-reference/authentication) * Explore [core concepts](/concepts)