Skip to main content

API Reference

DBBat provides a comprehensive REST API for managing users, databases, grants, and viewing observability data.

Base URL

All API endpoints are versioned under /api/v1/:

http://localhost:4200/api/v1

(The default API listen address is :4200DBB_LISTEN_API. Adjust the host/port to your deployment.)

OpenAPI Specification

The full OpenAPI 3.0 specification is available at:

  • Spec file: GET /api/openapi.yml
  • Interactive docs: GET /api/docs (Swagger UI)

Authentication

The API supports two authentication methods:

Basic Auth

HTTP Basic Authentication using username and password:

curl -u username:password http://localhost:4200/api/v1/users

Bearer Token

API key or web session token authentication:

curl -H "Authorization: Bearer <token>" http://localhost:4200/api/v1/users
note

API keys cannot create or revoke other API keys (security restriction) - these operations require Basic Auth or a web session token.

Roles

Users have one or more roles that determine their access:

RoleDescription
adminFull access to all resources and operations
viewerRead-only access to observability data (connections, queries, audit)
connectorCan only access databases they have active grants for

Password Change Requirement

Users must change their initial password before logging in. Login attempts from users who haven't changed their password will receive a 403 Forbidden response with error code PASSWORD_CHANGE_REQUIRED.

Users must then call PUT /auth/password with their username and credentials to change their password before they can log in.

Rate Limiting

General Rate Limiting

All authenticated endpoints are rate-limited. Response headers include:

HeaderDescription
X-RateLimit-LimitMaximum requests per minute
X-RateLimit-RemainingRemaining requests in current window
X-RateLimit-ResetUnix timestamp when limit resets

Authentication Rate Limiting

Failed login attempts are rate-limited per username with exponential backoff:

FailuresDelay
1-2No delay
3-45 seconds
5-630 seconds
7-92 minutes
10+5 minutes

Rate-limited authentication attempts receive a 429 Too Many Requests response with error code auth_rate_limited.

Pagination

List endpoints support pagination with limit and offset parameters:

curl -u admin:password "http://localhost:4200/api/v1/queries?limit=100&offset=0"
ParameterDefaultMinMaxDescription
limit10011000Maximum number of results
offset00-Number of results to skip

Response Format

All responses are JSON. Successful responses return the requested data:

{
"uid": "550e8400-e29b-41d4-a716-446655440000",
"username": "admin",
"roles": ["admin"],
"created_at": "2024-01-01T00:00:00Z"
}

Error responses include an error message:

{
"error": "user not found"
}

Health & Version

Health Check

GET /api/v1/health

Returns the health status of the API server and database connection. No authentication required.

Response:

{
"status": "healthy"
}

Version Info

GET /api/v1/version

Returns API version and build information. No authentication required.

Response:

{
"api_version": "v1",
"build_version": "1.2.3",
"build_commit": "abc1234",
"build_time": "2024-01-09T12:00:00Z"
}

Authentication Endpoints

Login

POST /api/v1/auth/login

Authenticates with username/password and creates a short-lived web session token (1 hour).

warning

Users who haven't changed their initial password will receive a 403 error. They must first change their password using PUT /auth/password.

Request:

{
"username": "admin",
"password": "secretpassword"
}

Response:

{
"token": "web_abc123...",
"expires_at": "2024-01-01T01:00:00Z",
"user": {
"uid": "550e8400-e29b-41d4-a716-446655440000",
"username": "admin",
"roles": ["admin"],
"password_change_required": false
}
}

Logout

POST /api/v1/auth/logout

Revokes the current web session token. Requires authentication.

Response: 204 No Content

Get Current User

GET /api/v1/auth/me

Returns information about the currently authenticated user and their session.

Response:

{
"uid": "550e8400-e29b-41d4-a716-446655440000",
"username": "admin",
"roles": ["admin"],
"password_change_required": false,
"session": {
"expires_at": "2024-01-01T01:00:00Z",
"created_at": "2024-01-01T00:00:00Z"
}
}

Change Password (Pre-login)

PUT /api/v1/auth/password

Allows users to change their password before logging in. No authentication required - validates username/current_password.

Request:

{
"username": "newuser",
"current_password": "temporary123",
"new_password": "newSecurePassword123!"
}

Response:

{
"message": "Password changed successfully"
}

Users

Create User

POST /api/v1/users

Creates a new user account. Requires admin role.

Request:

{
"username": "developer",
"password": "initialPassword123",
"roles": ["connector"]
}

Response:

{
"uid": "550e8400-e29b-41d4-a716-446655440000",
"username": "developer",
"roles": ["connector"],
"rate_limit_exempt": false,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}

List Users

GET /api/v1/users

Returns a list of users. Admins see all users; non-admins see only themselves.

Response:

{
"users": [
{
"uid": "550e8400-e29b-41d4-a716-446655440000",
"username": "admin",
"roles": ["admin"],
"rate_limit_exempt": true,
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
]
}

Get User

GET /api/v1/users/:uid

Retrieves a specific user by their UID.

Update User

PUT /api/v1/users/:uid

Updates a user account.

  • Non-admins can only update their own password
  • Non-admins cannot change roles
  • API keys cannot change passwords (requires Basic Auth)

Request:

{
"password": "newPassword123",
"roles": ["admin", "connector"]
}

Delete User

DELETE /api/v1/users/:uid

Deletes a user account. Requires admin role. Cannot delete your own account.

Change Password (Authenticated)

PUT /api/v1/users/:uid/password

Changes a user's password. Requires re-authentication via username/password in the request body.

Request:

{
"username": "admin",
"current_password": "oldPassword",
"new_password": "newSecurePassword123!"
}

API Keys

Create API Key

POST /api/v1/keys

Creates a new API key for the authenticated user. Requires Web Session or Basic Auth (API keys cannot create other API keys).

warning

The full API key is only returned once in this response. Store it securely as it cannot be retrieved later.

Request:

{
"name": "CI/CD Pipeline",
"expires_at": "2025-01-01T00:00:00Z"
}

Response:

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "CI/CD Pipeline",
"key": "dbb_abc123xyz...",
"key_prefix": "dbb_abc1",
"expires_at": "2025-01-01T00:00:00Z",
"created_at": "2024-01-01T00:00:00Z"
}

List API Keys

GET /api/v1/keys

Returns a list of API keys, scoped to the caller's own keys by default — including for admins. Admins can pass all_users=true to review every user's keys.

Query Parameters:

ParameterDescription
all_usersAdmin only: return every user's keys instead of just the caller's (default: false)
user_idFilter by user UID (admin only)
include_allInclude revoked and expired keys (default: false)

Get API Key

GET /api/v1/keys/:id

Retrieves a specific API key. Non-admins can only see their own keys.

Revoke API Key

DELETE /api/v1/keys/:id

Revokes an API key. Requires Web Session or Basic Auth (API keys cannot revoke API keys).

Response: 204 No Content


Servers

:::warning Renamed in v0.17.0 These endpoints were previously served under /api/v1/databases. The path is now /api/v1/servers and no alias is kept for the old one. The JSON response key is still databases. :::

Create Server

POST /api/v1/servers

Creates a new server configuration. Requires admin role.

Request:

{
"name": "production-main",
"description": "Main production database",
"protocol": "postgresql",
"host": "db.example.com",
"port": 5432,
"database_name": "myapp",
"username": "app_user",
"password": "dbPassword123",
"ssl_mode": "require"
}

protocol accepts postgresql (default), oracle, mysql, mariadb, or mongodb. For Oracle, also provide oracle_service_name so TNS clients can route by service name. For MongoDB, optionally set mongo_auth_source (defaults to admin).

Set via_uid to the UID of an SSH bastion to tunnel the upstream connection through it — see SSH servers below.

If the name is already taken, the request fails with 409 DUPLICATE_NAME.

Response:

{
"uid": "550e8400-e29b-41d4-a716-446655440000",
"name": "production-main",
"description": "Main production database",
"protocol": "postgresql",
"host": "db.example.com",
"port": 5432,
"database_name": "myapp",
"username": "app_user",
"ssl_mode": "require",
"oracle_service_name": null,
"via_uid": null,
"created_by": "660e8400-e29b-41d4-a716-446655440000"
}

List Servers

GET /api/v1/servers

Returns a list of database server configurations. SSH bastions are excluded — they are listed only under /api/v1/ssh-servers. Response varies by role:

RoleResponse
AdminFull details (host, port, database_name, username, ssl_mode, protocol, oracle_service_name, via_uid)
ViewerLimited info (uid, name, description)
ConnectorOnly servers with active grants (limited info)

Get Server

GET /api/v1/servers/:uid

Retrieves a specific server configuration. Response varies by role (same as list).

Update Server

PUT /api/v1/servers/:uid

Updates a server configuration. Requires admin role.

Request:

{
"description": "Updated description",
"host": "new-db.example.com",
"password": "newDbPassword123"
}

Delete Server

DELETE /api/v1/servers/:uid

Deletes a server configuration. Requires admin role.

To remove a tunnel without deleting the server, send "clear_via_uid": true on update — the server goes back to a direct dial.


SSH Servers

SSH bastions let DBBat reach upstreams that aren't directly routable. They are stored as servers with protocol ssh, but are kept out of the regular server listing and out of every grantable/connectable target context — you cannot grant access to a bastion, only through one.

List SSH Servers

GET /api/v1/ssh-servers

Returns the SSH bastion rows. Requires admin role. Secrets (private key, passphrase) are never returned.

{
"servers": [
{
"uid": "770e8400-e29b-41d4-a716-446655440000",
"name": "prod-bastion",
"protocol": "ssh",
"host": "bastion.example.com",
"port": 22,
"username": "ec2-user",
"ssh_known_host_key": "ssh-ed25519 AAAAC3Nza..."
}
]
}

Create SSH Server

POST /api/v1/ssh-servers

Requires admin role.

{
"name": "prod-bastion",
"host": "bastion.example.com",
"port": 22,
"username": "ec2-user",
"ssh_private_key": "-----BEGIN OPENSSH PRIVATE KEY-----\n...",
"ssh_passphrase": "optional"
}

ssh_private_key and ssh_passphrase are write-only and never returned. The bastion's host key is pinned on first successful connection (trust on first use) and reported back as the read-only ssh_known_host_key.

Tunnelled connections are pooled and shared across sessions, so many proxied clients reuse a single SSH connection per bastion. Tunnelling is supported for all four proxied protocols.


Grants

Create Grant

POST /api/v1/grants

Creates a new access grant for a user to a database. Requires admin role.

Request:

{
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"database_id": "660e8400-e29b-41d4-a716-446655440000",
"controls": ["read_only"],
"starts_at": "2024-01-01T00:00:00Z",
"expires_at": "2024-12-31T23:59:59Z",
"max_query_counts": 10000,
"max_bytes_transferred": 1073741824
}

controls is an array. Each element is one of:

ValueEffect
read_onlySQL inspection blocks writes; PostgreSQL also sets default_transaction_read_only = on.
block_copyBlocks COPY (PostgreSQL) and LOAD DATA / SELECT … INTO OUTFILE (MySQL).
block_ddlBlocks CREATE, ALTER, DROP, TRUNCATE.

An empty controls array (or omitting the field) grants full write access within the time window.

Response:

{
"uid": "770e8400-e29b-41d4-a716-446655440000",
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"database_id": "660e8400-e29b-41d4-a716-446655440000",
"controls": ["read_only"],
"granted_by": "880e8400-e29b-41d4-a716-446655440000",
"starts_at": "2024-01-01T00:00:00Z",
"expires_at": "2024-12-31T23:59:59Z",
"revoked_at": null,
"revoked_by": null,
"max_query_counts": 10000,
"max_bytes_transferred": 1073741824,
"query_count": 0,
"bytes_transferred": 0,
"created_at": "2024-01-01T00:00:00Z"
}

List Grants

GET /api/v1/grants

Returns a list of access grants. Connectors can only see their own grants.

Query Parameters:

ParameterDescription
user_idFilter by user UID
database_idFilter by database UID
active_onlyOnly return active (non-revoked, within time window) grants

Get Grant

GET /api/v1/grants/:uid

Retrieves a specific access grant. Connectors can only see their own grants.

Revoke Grant

DELETE /api/v1/grants/:uid

Revokes an access grant. Requires admin role.

Revocation takes effect immediately: further queries are blocked and any session already connected under that grant is disconnected, across all proxied protocols.


Grant Definitions

Definitions are admin-managed templates describing a shape of grant (controls, duration, quotas). Users can only request access by picking an active definition. Direct admin grant creation via POST /api/v1/grants bypasses definitions entirely.

Create Grant Definition

POST /api/v1/grant-definitions

Requires admin role.

{
"name": "read-only-1h",
"controls": ["read_only"],
"max_query_counts": 1000,
"max_bytes_transferred": 10485760,
"auto_approve": false
}

When auto_approve is true, requests against this definition skip the pending/admin-approval step entirely and the grant is materialized instantly at request time. Auto-approved requests still require a justification, are recorded in a dedicated audit trail, and post a Slack notification — without Approve/Deny buttons, since there is nothing to decide.

A duplicate name returns 409 DUPLICATE_NAME.

List Grant Definitions

GET /api/v1/grant-definitions

Non-admins always receive only active definitions. Admins receive both active and deactivated ones, unless active_only=true is passed.

Deactivated definitions have is_active: false; they are soft-deleted so historical grant requests keep referencing them.


Grant Requests

Submit a Grant Request

POST /api/v1/grant-requests

Any authenticated user can request access by selecting a definition and a server.

{
"grant_definition_id": "550e8400-e29b-41d4-a716-446655440000",
"database_id": "660e8400-e29b-41d4-a716-446655440000",
"justification": "Investigating ticket SUP-4821"
}

If the linked definition has auto_approve enabled, the response comes back already approved with resulting_grant_id populated — no admin action needed. Otherwise the request is pending.

Returns 409 if a pending request already exists for the same user/server/definition.

Approve / Deny / Cancel

POST /api/v1/grant-requests/:uid/approve
POST /api/v1/grant-requests/:uid/deny
POST /api/v1/grant-requests/:uid/cancel

Approval atomically transitions pending → approved and builds a real grant from the definition plus the request's user and server. Returns 409 if the request is no longer pending or its definition has been deactivated.

Admins can also approve a request and flip its definition to auto-approve in one action from the web UI, so future requests of the same shape are instant.

Status values: pending, approved, denied, cancelled, expired.


Connections

List Connections

GET /api/v1/connections

Returns a list of proxy connections. Connectors can only see their own connections.

Query Parameters:

ParameterDescription
user_idFilter by user UID
database_idFilter by database UID
limitMaximum results (default: 100, max: 1000)
offsetSkip results for pagination

Response:

{
"connections": [
{
"uid": "550e8400-e29b-41d4-a716-446655440000",
"user_id": "660e8400-e29b-41d4-a716-446655440000",
"database_id": "770e8400-e29b-41d4-a716-446655440000",
"source_ip": "192.168.1.100",
"connected_at": "2024-01-01T10:00:00Z",
"last_activity_at": "2024-01-01T10:30:00Z",
"disconnected_at": "2024-01-01T11:00:00Z",
"queries": 150,
"bytes_transferred": 1048576
}
]
}

Get Connection

GET /api/v1/connections/:uid

Retrieves a single connection. Connectors can only retrieve their own connections.

The web UI exposes this as a connection detail page, and the query detail breadcrumb links back to the connection a query belongs to.


Queries

List Queries

GET /api/v1/queries

Returns a list of executed queries. Requires admin or viewer role.

Query Parameters:

ParameterDescription
connection_idFilter by connection UID
user_idFilter by user UID
database_idFilter by database UID
start_timeFilter by start time (RFC3339 format)
end_timeFilter by end time (RFC3339 format)
limitMaximum results (default: 100, max: 1000)
offsetSkip results for pagination

Response:

{
"queries": [
{
"uid": "550e8400-e29b-41d4-a716-446655440000",
"connection_id": "660e8400-e29b-41d4-a716-446655440000",
"sql_text": "SELECT * FROM users WHERE id = $1",
"parameters": {
"values": ["123"],
"format_codes": [0],
"type_oids": [23]
},
"executed_at": "2024-01-01T10:15:00Z",
"duration_ms": 12.5,
"rows_affected": 1,
"error": null
}
]
}

Get Query

GET /api/v1/queries/:uid

Retrieves a specific query without its result rows. Requires admin or viewer role.

Use GET /queries/:uid/rows to retrieve the result rows.

Get Query Rows

GET /api/v1/queries/:uid/rows

Retrieves paginated result rows for a specific query. Requires admin or viewer role.

Uses cursor-based pagination with limits per request:

  • Maximum 1000 rows per response
  • Maximum 1MB of row data per response

The response stops at whichever limit is reached first.

Query Parameters:

ParameterDescription
cursorPagination cursor from previous response
limitMaximum rows (default: 100, max: 1000)

Response:

{
"rows": [
{
"row_number": 0,
"row_data": {"id": 1, "name": "Alice", "email": "alice@example.com"},
"row_size_bytes": 64
},
{
"row_number": 1,
"row_data": {"id": 2, "name": "Bob", "email": "bob@example.com"},
"row_size_bytes": 62
}
],
"next_cursor": "eyJvZmZzZXQiOjEwMH0=",
"has_more": true,
"total_rows": 500
}

Audit

List Audit Events

GET /api/v1/audit

Returns a list of audit log events. Requires admin or viewer role.

Query Parameters:

ParameterDescription
event_typeFilter by event type (e.g., user.created, grant.revoked)
user_idFilter by user UID (the user being acted upon)
performed_byFilter by performer UID (the user performing the action)
start_timeFilter by start time (RFC3339 format)
end_timeFilter by end time (RFC3339 format)
limitMaximum results (default: 100, max: 1000)
offsetSkip results for pagination

Response:

{
"audit_events": [
{
"uid": "550e8400-e29b-41d4-a716-446655440000",
"event_type": "user.created",
"user_id": "660e8400-e29b-41d4-a716-446655440000",
"performed_by": "770e8400-e29b-41d4-a716-446655440000",
"details": {
"username": "newuser",
"roles": ["connector"]
},
"created_at": "2024-01-01T00:00:00Z"
}
]
}

Error Responses

Standard Error

Every error response uses the same envelope:

{
"code": "VALIDATION_ERROR",
"message": "Human-readable explanation",
"detail": "Optional additional context",
"retry_after": 60
}
FieldDescription
codeMachine-readable error code (see below). Match on this, not on message.
messageHuman-readable explanation, safe to surface to users.
detailOptional extra context. Omitted when empty.
retry_afterSeconds to wait before retrying. Only present on RATE_LIMITED.

Error Codes

CodeTypical statusMeaning
VALIDATION_ERROR400Invalid input
UNAUTHORIZED401Authentication required
INVALID_CREDENTIALS401Wrong username or password
FORBIDDEN403Insufficient permissions
PASSWORD_CHANGE_REQUIRED403The user must change their password first
WEAK_PASSWORD400Password does not meet requirements
NOT_FOUND404Resource does not exist
CONFLICT409State conflict (e.g. transitioning a non-pending grant request)
DUPLICATE_NAME409A resource with that name already exists
TARGET_MATCHES_SELF400The target points at DBBat's own storage database
GRANT_EXPIRED403The access grant has expired
QUOTA_EXCEEDED403A usage quota was exceeded
RATE_LIMITED429Too many requests; see retry_after
OAUTH_FAILED401OAuth authentication failed
OAUTH_STATE_MISMATCH401Invalid or expired OAuth state
OAUTH_PROVIDER_ERROR401The OAuth provider returned an error
OAUTH_USER_NOT_LINKED401No account is linked to that OAuth identity
OAUTH_WRONG_WORKSPACE401The wrong OAuth workspace was used
INTERNAL_ERROR500Unexpected server error (details are logged, never returned)

Rate Limit Error

{
"code": "RATE_LIMITED",
"message": "Too many requests. Try again later.",
"retry_after": 60
}

A Retry-After header carries the same value.