DB DevOps API
ChangeOps surface: scripts, releases, target connections, executions, approvals, and the audit trail. Base path /api/db-devops, needs an active license, authenticated. Governed by the 5000/min DB DevOps limiter. This surface persists to Postgres via the repository layer; the legacy in-memory /api/migrations environment endpoints (IDE side) are a separate, non-durable subsystem.
Scripts — /api/db-devops/scripts
| Method | Endpoint | Description |
|---|---|---|
| GET | /scripts | List scripts with status, execution mode, environment, target |
| GET | /scripts/:id | Single script, including its revision, which increases by one whenever its SQL or rollback SQL changes |
| POST | /scripts | Create (draft) |
| PUT | /scripts/:id | Partial update (only supplied fields change) |
| DELETE | /scripts/:id | Delete; 409 if referenced by a release/execution/approval |
| POST | /scripts/:id/approve | { "approvedBy": "email" }; notifies author |
| POST | /scripts/:id/reject | { "rejectedBy": "email", "reason": "..." }; author may edit and resubmit |
| GET | /scripts/:id/blast-radius | Latest computed blast-radius report (200 with null if none computed yet) |
| POST | /scripts/:id/blast-radius | Compute/recompute now; optional { "approvalRequestId": "uuid" }. 409 if no target connection assigned |
| POST | /scripts/:id/shadow-run | Start an async shadow execution (202); poll /shadow-runs/:id |
| GET | /scripts/:id/shadow-runs | Shadow runs of the script, newest first |
| GET | /scripts/:id/blast-radius/narrative | AI plain-English narration of the report (needs a configured AI provider) |
POST /api/db-devops/scripts
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | ≤ 500 chars |
sql_content | string | Yes | SQL or Mongo shell commands; ≤ 5MB (413 over cap) |
execution_mode | string | No | release (default) or asap |
environment | string | Yes | An existing environment, matched in any letter case and stored under its own name. Missing or unknown: 400 INVALID_SCRIPT_TARGET listing the environments |
target_connection_id | uuid | No | An existing connection (400 INVALID_SCRIPT_TARGET otherwise) in the script's environment or in none; one in another environment is 400 TARGET_IN_OTHER_ENVIRONMENT naming both. Checked again whenever the environment or target changes; may be assigned later, re-validated before execution |
target_database | string | No | Database name on the target instance |
author | string | No | Falls back to the authenticated caller |
curl -X POST http://localhost:5002/api/db-devops/scripts \\
-H "Authorization: Bearer YOUR_TOKEN" -H "Content-Type: application/json" \\
-d '{ "name": "add-users-index",
"sql_content": "CREATE INDEX idx_users_email ON users(email);",
"execution_mode": "release", "environment": "production" }'
// 201 -> full script row; approval_status "pending". Blast radius is computed asynchronously.
PUT /scripts/:id checks a new environment or target_connection_id the same way. Other failures answer 500 with SCRIPT_CHANGE_FAILED and a fixed message; the server log has the cause.
Releases — /api/db-devops/releases
| Method | Endpoint | Description |
|---|---|---|
| GET | /releases | List releases |
| GET | /releases/:id | Single release; 404 for an unknown id |
| GET | /releases/:id/scripts | Ordered scripts in the release; 404 for an unknown release |
| GET | /releases/:id/runs | The release's runs, newest first: status, environment, connection, who started it and when |
| POST | /releases | Create (Release Manager and above); body below |
| PUT | /releases/:id | Update any of the body fields; sending script_ids replaces the release's scripts in the given order |
| DELETE | /releases/:id | Delete (admin) |
| POST | /releases/:id/signoff | Record a release-level sign-off (Approver and above) |
| Field | Type | Required | Notes |
|---|---|---|---|
name | string | Yes | Up to 255 characters |
version | string | No | Up to 50 characters; generated as vYYYYMMDD-HHMMSS (UTC) when omitted |
description | string | No | |
target_environment | string | No | Environment name, up to 50 characters |
target_grouping | string | No | Grouping or connection name, up to 100 characters |
scheduled_execution_date | ISO 8601 date-time | No | Planned date |
script_ids | uuid[] | No | Existing scripts, each once, in run order. Refused with the reason: scripts whose target connections are different database types, and scripts whose target connections are in different environments. On an update, a script added to a release must target the environment of the release's targeted scripts; scripts without a target fit any release |
Invalid fields answer 400 with the reason. The sign-off status, the signed-off fields and executed_at are set only by sign-off and by running the release; a create or update that tries to change them answers 400 "Sign-off status changes only through sign-off".
Connections — /api/db-devops/connections
Target databases for execution. The connection record's config JSONB holds dialect-specific fields plus Manager metadata (groupings, environment), so the IDE and DB DevOps share one connection list. Passwords are encrypted at rest and masked on read.
| Method | Endpoint | Description |
|---|---|---|
| GET | /connections | List |
| GET | /connections/importable | IDE connections eligible for import |
| POST | /connections/import | Import selected IDE connections |
| GET | /connections/:id | Single (masked) |
| GET | /connections/environment/:env | Filter by environment |
| GET | /connections/grouping/:grouping | Filter by grouping |
| POST / PUT / DELETE | /connections · /connections/:id | CRUD; a type other than the saved engine is 400 ENGINE_CHANGE |
| POST | /connections/:id/test | Connectivity probe |
| GET | /connections/:id/databases | The instance's databases, read through its engine (below) |
GET /api/db-devops/connections/:id/databases
Lists the instance's databases through its engine. listed is false for engines without a list of databases, with a message saying the connection's database is used.
{ "listed": true, "databases": [ { "name": "0", "size_bytes": null, "detail": "12 keys", "system": false } ] }
{ "listed": false, "databases": [], "message": "Cassandra has no list of databases; scripts for Demo Cassandra run against the database set on the connection" }
| Status | Code | Meaning |
|---|---|---|
| 404 | CONNECTION_NOT_FOUND | No such connection, or a malformed id |
| 502 | CONNECTION_FAILED | The instance could not be reached or listed; the message gives the reason |
| 500 | DATABASES_UNREADABLE | Any other failure; the server log has the cause |
// POST /connections/:id/test
{ "success": true, "message": "Connection successful", "latency_ms": 45 }
{ "success": false, "message": "ECONNREFUSED 10.0.0.5:5432" }
Databases are read through the engine adapter (PostgreSQL pg_database, MySQL information_schema.schemata, SQL Server sys.databases, MongoDB listDatabases, Oracle v$pdbs, Redis numbered databases), using the connection's settings including SSH tunnels.
Executions — /api/db-devops/executions
The server writes an execution record only when it runs SQL against a target (one per script run, including each script of a release run), opening a driver pool (pg / mysql2 / mssql / oracledb / mongodb) after decrypting the target password. Records cannot be created or changed through the API; only a rollback changes one.
| Method | Endpoint | Description |
|---|---|---|
| GET | /executions | Full history: status, timing, rows, output, error, and what ran (executed_sql, sql_sha256, script_revision, run_mode, target_database, rollback_sql) |
| GET | /executions/recent | Most recent runs; limit from 1 to 100 (default 10), anything else is 400 |
| GET | /executions/:id | Single record. A run in progress has status: RUNNING, heartbeat_at (the server confirms the run every 15 seconds) and progress: { "phase": "starting | checks | rollback | statements | reversing", "total", "done", "failed", "current": { "index", "preview", "startedAt" } }, where total is the number of statements in the phase (null before it is known) and current is the statement running, or null between statements. progress.engine is what the database reports about the statement running: { "label", "status": "reporting | waiting | nothing | unreadable | unsupported", "text", "fraction", "readAt" }, where fraction (0 to 1) is the database's own share done when it gives one. A completed script run also has rollback_capture: { rollbackSql, irreversible, notes }. A run whose server stops is set to FAILED after 90 seconds without a heartbeat |
| GET | /deployments/active | The runs in progress: { "success": true, "data": [{ "id", "scriptName", "releaseName", "environment", "connectionName", "database", "startedAt", "startedBy", "progress" }] }; a failed read is 500 with the reason |
| POST | /executions/run | Execute one script live against a target; 409 when a release script is not approved, when an ad-hoc script that is not approved targets an environment that requires approval, or naming any dependency that has not completed a run. The script's -- @check lines are evaluated first, read-only, on the same connection and database; if one fails or cannot be evaluated the SQL is not run and the run is recorded FAILED with the reason |
| POST | /executions/run-release | Run a signed-off release: scripts in dependency order, stopping at the first failure. 409 when the release is not signed off, is already running, its scripts changed after sign-off, the run would touch more than one environment, or its environment needs an earlier run or an approval it does not have yet |
| POST | /executions/:id/rollback | Roll back a completed run: body { "reason": "..." } (required, up to 2,000 characters). Runs the rollback captured for the run against the same connection and database as a new run with rollback_of set, then marks the original ROLLED_BACK with rolled_back_by, rolled_back_at and the reason. Answers { execution, rollback } when it ends; with "wait": false it answers 202 at once with the original (ROLLING_BACK) and the rollback run (RUNNING). A rollback that fails is rollback.status = FAILED and the original stays COMPLETED. 409 for a run that is not completed, already rolled back, itself a rollback, has no rollback SQL, has a rollback of comments only (nothing would be undone), or lost its connection |
A malformed or unknown execution id answers 404 on every /executions/:id route, and /executions/release-progress/:releaseId answers 404 for a release that does not exist. Both run routes take an optional mode of transactional, halt or continue; any other value is refused with 400 before anything runs. A run continues on the server whether or not the request that started it is still open: with "wait": false the run, release and rollback routes answer 202 as soon as the run has started, and a run that takes longer than a proxy or load balancer allows for one request still finishes and is recorded. Checks that refuse a run (400, 404, 409) answer before anything starts either way. A run naming a script or connection that does not exist is 404 and leaves no record.
POST /api/db-devops/executions/run
// body
{ "script_id": "uuid", "connection_id": "uuid", "database": "optional-override", "mode": "transactional | halt | continue", "wait": false }
// 202 with "wait": false: the record, status RUNNING, at once; read GET /executions/:id for its progress and outcome
{ "id": "uuid", "status": "RUNNING", "progress": null, "heartbeat_at": "2026-10-21T15:00:00Z" }
// otherwise the answer waits for the run. 200 when the run completes: its execution record, the engine output and the rollback captured before it ran ({ rollbackSql, irreversible, notes })
{ "id": "uuid", "status": "COMPLETED", "rows_affected": 1, "execution_time_ms": 45,
"output": "1 statement(s) executed. 1 row(s) affected.",
"executed_sql": "...", "sql_sha256": "...", "script_revision": 3, "target_database": "app",
"rollback": { "rollbackSql": "DROP TABLE IF EXISTS \\"users\\";", "irreversible": [], "notes": [] } }
// 200 when the SQL fails: the record with status FAILED and the driver error
{ "id": "uuid", "status": "FAILED", "error_message": "relation \\"users\\" already exists" }
// 400 unknown mode, 404 unknown script or connection, 409 not approved or a dependency has not run
POST /api/db-devops/executions/run-release
Each script's -- @check lines are evaluated before its SQL, and a failing check stops the release like any failed script. Each script runs against its own target_connection_id/target_database, falling back to the request's connection_id; the request's database applies only to scripts that run on that connection without a target database of their own. Each script's execution record names the connection and database it actually ran against. Aggregate status is FAILED if any script fails; the per-script results[] pinpoints where it stopped.
{ "release_id": "uuid", "connection_id": "uuid", "database": "optional", "wait": false }
// 202 with "wait": false, at once
{ "release_id": "uuid", "run_id": "uuid", "status": "RUNNING", "scripts_total": 2 }
// GET /executions/release-progress/:releaseId?since=<epoch ms> then gives each script's execution_id, status, started_at and progress, and run: { id, status, error_message, started_at, finished_at }
// otherwise the answer waits for the run. 200
{ "status": "COMPLETED", "results": [
{ "script_id": "abc", "script_name": "create-users-table", "execution_id": "uuid", "status": "COMPLETED", "execution_time_ms": 32 },
{ "script_id": "def", "script_name": "add-users-index", "execution_id": "uuid", "status": "COMPLETED", "execution_time_ms": 18 } ] }
// partial failure
{ "status": "FAILED", "results": [
{ "script_id": "abc", "status": "COMPLETED", "execution_time_ms": 32 },
{ "script_id": "def", "status": "FAILED", "error": "syntax error at or near \\"SELEC\\"" } ] }
Approvals — /api/db-devops/approvals
Multi-signature approval workflow over approval_requests + approval_signatures. A request is satisfied when the required signatures are collected. A malformed or unknown request id answers 404 on every /approvals/:id route.
| Method | Endpoint | Description |
|---|---|---|
| GET | /approvals | List requests |
| GET | /approvals/policy | The approval policy in effect: { "allowSelfApproval": true, "defaultRequiredApprovers": 1 } until an admin changes either (settings allow_self_approval, true or false, and default_required_approvers, a whole number from 1 to 10) |
| GET | /approvals/:id | Single request with signatures, and approved_at, or rejected_by, rejected_at and rejection_reason once decided |
| GET | /approvals/status/:status | Filter by status: pending, approved or rejected, in any case; anything else is 400 |
| GET | /approvals/:id/blast-radius | Blast-radius report bound to the request |
| GET | /approvals/:id/blast-radius/narrative | AI narration (needs a configured AI provider) |
| GET | /approvals/:id/shadow-runs | Shadow runs for the request |
| GET | /approvals/:id/snapshots | Decision snapshots of the request, newest first |
| POST | /approvals/:id/snapshots | Capture a decision snapshot now (approver); returns it (201) |
| POST | /approvals | Open a request for one script (script_id) or one release (release_id plus the environment it is for, which must exist; the release must be signed off). A script request is for the script's own environment: an environment naming another one is 400 with the reason, and none sent takes the script's (a script without an environment takes an existing one by its own name). Optional: required_approvers from 1 to 10 (the default required approvers when omitted), urgency (LOW, MEDIUM, HIGH, CRITICAL), business_justification and rollback_plan (up to 10,000 characters each), testing_evidence (up to 20 text items of 500 characters). A new request is always pending; a script request records the script's script_revision and the SHA-256 of its SQL (script_sql_sha256) and of its rollback SQL (script_rollback_sha256). 409 when the script, or the release in that environment, already has a pending request (for a script, one made for its current SQL and rollback SQL). Everyone who can sign (Approvers, Release Managers and admins) is notified. |
| POST | /approvals/:id/sign | Add your signature (Approver and above), with optional comments up to 2,000 characters. When the request has enough signatures it becomes approved, and a script request approves the script. 409 if you already signed, the request is not pending, or the script's SQL or rollback SQL changed after the request was made |
| POST | /approvals/:id/reject | Reject a pending request with a reason (required, up to 2,000 characters). The reviewer, the time and the reason are saved on the request; a script request also rejects the script |
| POST | /approvals/:id/revoke | Revoke an approved request: its signatures are cleared and it returns to pending; a script request returns the script to pending, along with any signed-off release that contains it |
| DELETE | /approvals/:id/sign/:email | Withdraw a signature while the request is pending (your own, or anyone's as an admin) |
| DELETE | /approvals/:id | Withdraw a pending request (the person who requested it, or an admin). An approved or rejected request is part of the approval record and cannot be deleted (409) |
Environments and Groupings — /api/db-devops/environments, /api/db-devops/groupings
Every role can read both lists; creating, changing and deleting needs the Release Manager or Admin role. Names are unique regardless of case; an environment name is at most 50 characters and a grouping name at most 100. A refusal is { "error": "..." } with the reason.
| Method | Endpoint | Description |
|---|---|---|
| GET | /environments | List: id, name, requiresApproval, promoteFrom (the environment a release must complete a run in first, or null), groupings, approvers |
| POST | /environments | Create with name and optional requiresApproval, promoteFrom and groupings; 201 with the environment. 400 for a blank or too long name, an unknown grouping or an unknown promoteFrom; 409 for a name in use or a grouping of another database type |
| PUT | /environments/:id | Change any of name, requiresApproval, promoteFrom and groupings. A rename also renames the environment on every connection, script, release, approval request and compliance rule that names it (referencesUpdated counts them). Sending groupings replaces the list and moves connections in the same transaction (see below). 400 when promoteFrom names the environment itself or would make a loop; 404 for an unknown id; 409 for a name in use or a grouping of another database type |
| DELETE | /environments/:id | 409 while another environment runs only after it, or while any connection or script uses it; the message names what is in the way |
| GET | /groupings | List: id, name, description |
| POST | /groupings | Create with name and optional description; 201, or 409 for a name in use |
| PUT | /groupings/:id | Rename; every connection, environment and release that used the old name is updated (connectionsUpdated) |
| DELETE | /groupings/:id | 409 while any connection belongs to it; deleting an unused grouping also removes it from the environments it was assigned to |
A connection belongs to one environment. When PUT /environments/:id adds a grouping, every connection in that grouping moves into the environment, out of any other one; when it removes a grouping, the connections it brought in leave the environment unless another grouping still assigned there covers them. The response lists what changed:
{ "id": "env_...", "name": "Staging", "groupings": ["us-east"], ...,
"referencesUpdated": 0,
"moved": [ { "id": "...", "name": "orders-east", "from": "Development" } ],
"unassigned": [] }
Assigning a grouping that holds another database type, or one that mixes types, is 409: a script is sent to every target in an environment as written, so an environment holds one database type.
The same rule applies from the connection side. When a connection is created or saved (POST/PUT /api/connections, /api/connections/bulk, POST/PUT /api/db-devops/connections) with a grouping it was not in before, and that grouping is assigned to exactly one environment, the connection is saved in that environment. It is 409 (a failed row in bulk upload) when an environment the grouping is assigned to targets another database type, when the new groupings belong to two different environments, or when the same request places the connection in another environment. A grouping assigned to several environments only checks the database type.
Audit — /api/db-devops/audit (administrators)
Append-only, tamper-evident audit trail. Every mutating request is recorded by auditMiddleware with sensitive fields redacted, and every entry records the client address (see TRUST_PROXY). Reading needs an administrator, or a role that grants audit:read (Read and export the Audit Log), and an active license; other accounts get 403 Insufficient permissions. The purge is the only permitted deletion, and only an administrator can run it.
| Method | Endpoint | Description |
|---|---|---|
| GET | /audit | Paginated log, newest first (?limit=&offset=, default 100) |
| GET | /audit/search | Filtered log: ?users= and ?actions= (comma-separated), start and end (ISO), limit, offset. A date it cannot read is refused with 400 Invalid start or end date |
| GET | /audit/facets | The accounts and actions recorded, { "users": [], "actions": [] } |
| GET | /audit/user/:email | By actor |
| GET | /audit/action/:action | By action |
| GET | /audit/entity/:type/:id | By entity |
| GET | /audit/range?start=&end= | By date range (ISO) |
| GET | /audit/count | { "count": n } |
| GET | /audit/summary?olderThanDays= | { "count": n, "oldest": ISO or null, "older": n or null }: every entry, the oldest one, and, when olderThanDays (a whole number, 1 or more) is given, how many are older than that; any other value is 400 |
| GET | /audit/integrity | Verify the hash chain over the entire trail |
| GET | /audit/export?format=csv|json | Every entry matching users, actions, start and end (the same filters as /audit/search), oldest first, streamed as a download with sequence, time, user, action, entity, client address, before and after values and the chain hashes; the export is recorded as EXPORT_AUDIT_LOG. An unknown format or date is 400 |
| GET | /audit/evidence?start=&end=&environment= | The change-control evidence for a period and, optionally, one environment, as a zip (application/zip) of change-control-report.html, changes.csv, evidence.json and SHA256SUMS. start and end are dates (YYYY-MM-DD, whole UTC days, end included) or ISO 8601 times; 400 when either is missing or start is not before end. Records EXPORT_CHANGE_EVIDENCE with the SHA-256 of the zip. |
| GET | /audit/stream | The audit stream's settings (without the header value) and delivery status: { configured, destination, config, hasSecret, state, deliveredSeq, lastSeq, backlog, failures, lastAttemptAt, lastSuccessAt, lastError, nextAttemptAt }, where state is off, delivering, up_to_date or failing |
| PUT | /audit/stream | Admin only. { destination: "https", url, format: "splunk-hec" | "json", headerName?, headerSecret?, appName?, ca?, startFrom?: "now" | "beginning" } or { destination: "syslog", host, port?, appName?, ca?, startFrom? }; a blank headerSecret keeps the saved one. 400 names a bad value |
| DELETE | /audit/stream | Admin only. Turns the stream off |
| POST | /audit/stream/test | Admin only. Sends one AUDIT_STREAM_TEST event; 502 with the receiver's answer when it is not delivered |
| DELETE | /audit/cleanup?days=90 | Removes the entries older than days (a whole number, 1 or more; 90 when omitted) and returns { "deleted": n }; any other value is refused with 400 |
GET /api/db-devops/audit/integrity
Recomputes the tamper-evident hash chain and reports whether the trail is intact. Any break identifies the first offending row so an operator can locate the tampering or gap.
{ "valid": true, "brokenSeq": null, "total": 10432 }
Settings — /api/db-devops/settings
K/V store (approval policy, session and password rules, identity providers). Any signed-in user can read settings; only admins can change them. Values of secret settings (API keys, secret access keys, tokens, and credential headers) are write-only: reads and the save response return [REDACTED] in their place.
| Method | Endpoint | Description |
|---|---|---|
| GET | /settings | All settings |
| GET / PUT / DELETE | /settings/:key | Read / upsert / remove a single key |
| POST | /settings/batch | Upsert many keys atomically |
| GET | /settings/policy/security | Admin. { "values": {...}, "ranges": {...} }: the Session & Security values in effect, defaults included, and the range each number accepts |
| GET | /settings/policy/mail | Admin. { "configured", "via", "from", "messages": [{ "name", "to" }], "alertWebhookHost" }: how this server sends email and which messages |
The Session & Security keys are checked on PUT and batch, and a value outside its range is refused with 400 naming the key and the range (a batch is refused whole): sessionTimeoutMinutes 1 to 1440, maxLoginAttempts 1 to 100, lockoutDurationMinutes 1 to 1440, minPasswordLength 12 to 200, and enforcePasswordComplexity and autoLogoutOnInactivity true or false.
Discovery — /api/db-devops/discovery
Fleet inventory: hosts, scans, discovered databases, and tags. A scan connects with the host's stored credentials and lists what the engine holds: databases on PostgreSQL, TimescaleDB, CockroachDB, MySQL, MariaDB, SQL Server and MongoDB (with their size where the engine reports it), schemas (users) on Oracle (the host needs its service_name), keyspaces on Cassandra, databases on Neo4j, logical databases with their key counts on Redis, and indices with their document count and size on Elasticsearch. A SQLite host is refused with 400, since SQLite has no server to scan. Host responses never include the stored password; password_set says whether one is stored. A scan that cannot connect answers 400 with the engine's reason and records it on the host. Reads are open to signed-in users; every change needs the Release Manager or Admin role (403 otherwise) and the Configuration section.
| Method | Endpoint | Description |
|---|---|---|
| GET / POST / PUT / DELETE | /discovery/hosts · /discovery/hosts/:id | Host CRUD. scan_schedule is manual, hourly, every_6_hours, daily or weekly; each host comes back with next_scan_at (null for manual) |
| POST | /discovery/hosts/:id/scan | Scan a host for databases |
| GET | /discovery/hosts/:id/history | Scan history |
| GET | /discovery/databases · /discovery/databases/:id | Discovered databases |
| PUT | /discovery/databases/:id/status | Update inventory status |
| POST / DELETE | /discovery/databases/:id/tags · /tags/:tagId | Tag assignment |
| POST | /discovery/databases/:id/connection | Adds a found database as a DB DevOps connection: { name?, environment? }. Uses the server's credentials, is tested and placed like POST /api/connections, marks the database managed and returns 201 { database, connection }; 409 when it already is a connection |
| GET / POST | /discovery/tags | Tag catalog |
| GET / POST / PUT / DELETE | /discovery/aws-accounts · /discovery/aws-accounts/:id | AWS accounts Discovery reads: { name, role_arn?, external_id?, regions, access_key_id?, secret_access_key?, endpoint_url?, db_username?, db_password?, scan_schedule? }, a role, an access key or both; secrets come back only as secret_access_key_set and db_password_set |
| POST | /discovery/aws-accounts/:id/sync | Reads RDS instances and Aurora clusters in each region and registers them as hosts: { registered, updated, retired, skipped, servers }; 502 with AWS's error when the role or a region cannot be read |
Metrics — /api/db-devops/metrics
GET /api/db-devops/metrics?days=30&tz=America/New_York returns the numbers behind the Metrics screen for one time window. Any active license, signed-in callers, any role. days is 7, 30 (the default), 90 or 365; anything else is 400 with code INVALID_METRICS_WINDOW. tz names the time zone the days are counted in (the Metrics screen sends the browser's); without it days follow the database server's date. A tz that is not a time zone name, or that the database does not know, is 400 with code INVALID_TIME_ZONE. 7, 30 and 90 days are counted per day; 365 is counted per month over the current month and the eleven before it. Every series has one entry per period, including periods with no activity. If the database cannot be read the answer is 500 with code METRICS_FAILED.
{ "window": { "days": 30, "bucket": "day", "since": "2026-09-08", "until": "2026-10-07", "timeZone": "America/New_York" },
"deployment": { "runs": 10, "completed": 6, "failed": 2, "other": 2,
"successRate": 75, "avgDurationMs": 31,
"byStatus": [{ "name": "COMPLETED", "count": 6 }],
"series": [{ "period": "2026-10-07", "completed": 6, "failed": 2, "other": 2 }],
"byConnection": [{ "connection": "Orders", "runs": 7, "avgMs": 40 }],
"failureCauses": [{ "name": "relation \\"orders_old\\" does not exist", "count": 2 }] },
"releases": { "total": 5, "created": 2, "signedOff": 1, "byStatus": [...], "series": [{ "period", "created", "signedOff" }], "largest": [{ "release", "scripts" }] },
"approvals": { "requests": 6, "decided": 4, "rejected": 1, "rejectionRate": 25, "avgHoursToDecision": 2.5,
"approvers": 2, "byStatus": [...], "topApprovers": [{ "name", "count" }], "series": [{ "period", "requests", "rejected", "hours" }] },
"scripts": { "total": 12, "created": 4, "contributors": 2, "byMode": [...], "topContributors": [...], "series": [{ "period", "count" }] },
"system": { "users": 5, "activeUsers": 4, "connections": 5, "connectionsBySurface": [{ "name": "devops", "count": 3 }],
"auditEvents": 10, "auditSeries": [{ "period", "count" }] } }
successRateis completed runs over completed and failed runs, to one decimal;othercounts runs that are pending, running or rolled back.successRate,avgDurationMs,rejectionRateandavgHoursToDecisionarenullwhen there is nothing to measure.avgDurationMsandbyConnection[].avgMsaverage completed runs. Runs on a deleted connection are reported as"Deleted connection".failureCausesgroups failed runs by the database's error message, without the runner's "Statement N failed" prefix, cut at 160 characters, up to ten.- Runs are dated by when they started; scripts, releases and audit entries by when they were created; approvals by when they were requested.
periodisYYYY-MM-DD, orYYYY-MMfor 365 days. totalfor releases and scripts, and everything undersystemexcept the audit counts, cover all time rather than the window.
Notifications — /api/notifications
| Method | Endpoint | Description |
|---|---|---|
| GET | /notifications?limit=&offset= | The current user's notifications, newest first; limit up to 100 (default 50) |
| GET | /notifications/unread-count | Unread badge count: { count } |
| POST | /notifications/:id/read · /:id/unread | Mark one notification read or unread |
| POST | /notifications/read-all | Mark every notification read: { markedAsRead } |
| DELETE | /notifications/:id | Dismiss one notification |
| DELETE | /notifications/read | Clear every read notification: { deleted } |
| GET / PUT | /notifications/preferences | The current user's choices: { approval_requested, approval_signed, script_approved, script_rejected, release_deployed, snapshot_problem }, each true or false. A database failure answers 500 with a plain message, such as The notification settings could not be read. A kind set to false is not created for that user. Unknown kinds or values that are not booleans are 400 |
Notifications always belong to the signed-in user; another user's notification, or a malformed id, is 404. Each notification carries the id of its item in metadata (scriptId, approvalId or releaseId) and an action_url of the form /?app=manager&view=scripts|approvals|releases&open=<id>.
The backend also emits these over Socket.IO (role-aware broadcast); polling these endpoints is the supported HTTP fallback.
Data protection — /api/data-protection
Subject-rights operations over user data.
| Method | Endpoint | Description |
|---|---|---|
| GET | /data-protection/export/:userId | Export a user's data (portability) |
| GET | /data-protection/access/:userId | Access report |
| DELETE | /data-protection/erase/:userId | Erasure |
IDE routes (any active license)
The interactive IDE calls the connection/query/schema families directly. Connections are shared with DB DevOps.
| Method | Endpoint | Description |
|---|---|---|
| GET / POST | /api/connections | List / create (backend tests reachability, then encrypts password, before persisting) |
| GET / PUT / DELETE | /api/connections/:id | Read (masked) / update / delete. A type other than the saved engine is 400 ENGINE_CHANGE |
| POST | /api/connections/test | Probe without persisting |
| POST | /api/connections/:id/connect · /disconnect | Open / close a live pool. When the database refuses the connection, connect is 502 CONNECTION_FAILED with Could not connect to "<name>": <reason> |
| GET | /api/connections/:id/databases | The connection's databases, read through its engine with the stored settings (SSH tunnel, SSL mode), with each database's size where the engine reports one |
| POST | /api/queries/execute | Run a query |
| POST | /api/queries/execute-batch · /run-script | Multi-statement / script execution |
| POST | /api/queries/preview | How much a write would affect without keeping it: { rowsAffected, unit, committed: false }, where unit is rows, documents, keys or changes (Neo4j); a read answers { read: true } and is not run |
| GET | /api/databases/:connectionId/schema | Schema introspection |
| GET | /api/databases/:connectionId/object-definition | Current definition of one object (below) |
| POST | /api/databases/:connectionId/plan | The query plan as a tree: { query, database?, analyze? } returns { success, engine, roots, analyzed, summary, raw }, where each node has operation, object, detail, estRows, cost, actualRows, actualMs, loops, buffers and children. 400 ONE_STATEMENT, 400 ANALYZE_UNSUPPORTED (analyze off the PostgreSQL and MySQL families), 409 ANALYZE_NEEDS_READ, 409 NO_QUERY_PLAN (Redis), 400 PLAN_FAILED with the database's error |
| POST | /api/databases/:connectionId/rows | Every row a statement returns, without a row limit: { query, database? }. A statement that writes is refused with 409 EXPORT_NEEDS_READ |
| POST | /api/databases/:connectionId/insert-script | Insert statements in the engine's own language: { table, schema?, keyColumns?, rows }, or { table, schema?, keyColumns?, query, database? } to read the rows first (a writing query is 409 EXPORT_NEEDS_READ). Returns { success, script, rows }; 400 INSERT_SCRIPT_INVALID names a missing table, key column or Elasticsearch _id |
| GET | /api/navigator/:connectionId/objects · /object-properties · /tables/:schema/:name/columns | Object Explorer tree + inspector (per-dialect) |
GET /api/databases/:connectionId/object-definition
Reads the current definition of one object (the browser calls it as /api/object-definition?connectionId=…). Query parameters: objectType, objectName, and optionally schemaName and database (another database on the same instance). Names are sent to the database as values, never as SQL.
{ "definition": "CREATE OR REPLACE VIEW app.recent_orders AS ..." }
{ "definition": null, "message": "The view app.recent_orders does not exist in shop" }
| Status | Code | Meaning |
|---|---|---|
| 400 | UNSUPPORTED_OBJECT_TYPE | The engine cannot describe that type; the message lists the types it can |
| 400 | DEFINITIONS_UNSUPPORTED | The engine has no object definitions (MongoDB, Redis, Cassandra, Neo4j, Elasticsearch) |
| 400 | VALIDATION_ERROR | No object name |
| 404 | CONNECTION_NOT_FOUND | No such connection, or a malformed id |
| 500 | DEFINITION_UNREADABLE | The lookup failed; the message gives the database's reason |
// POST /api/queries/execute
{ "connectionId": "conn-uuid", "query": "SELECT * FROM users LIMIT 10" }
// ->
{ "data": [ { "id": 1, "email": "admin@company.com" } ],
"columns": ["id","email"], "rowCount": 1, "executionTime": 12 }
executionTime is server-measured database time in ms (excludes browser round-trip).