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

MethodEndpointDescription
GET/scriptsList scripts with status, execution mode, environment, target
GET/scripts/:idSingle script, including its revision, which increases by one whenever its SQL or rollback SQL changes
POST/scriptsCreate (draft)
PUT/scripts/:idPartial update (only supplied fields change)
DELETE/scripts/:idDelete; 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-radiusLatest computed blast-radius report (200 with null if none computed yet)
POST/scripts/:id/blast-radiusCompute/recompute now; optional { "approvalRequestId": "uuid" }. 409 if no target connection assigned
POST/scripts/:id/shadow-runStart an async shadow execution (202); poll /shadow-runs/:id
GET/scripts/:id/shadow-runsShadow runs of the script, newest first
GET/scripts/:id/blast-radius/narrativeAI plain-English narration of the report (needs a configured AI provider)

POST /api/db-devops/scripts

FieldTypeRequiredDescription
namestringYes≤ 500 chars
sql_contentstringYesSQL or Mongo shell commands; ≤ 5MB (413 over cap)
execution_modestringNorelease (default) or asap
environmentstringYesAn 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_iduuidNoAn 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_databasestringNoDatabase name on the target instance
authorstringNoFalls 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

MethodEndpointDescription
GET/releasesList releases
GET/releases/:idSingle release; 404 for an unknown id
GET/releases/:id/scriptsOrdered scripts in the release; 404 for an unknown release
GET/releases/:id/runsThe release's runs, newest first: status, environment, connection, who started it and when
POST/releasesCreate (Release Manager and above); body below
PUT/releases/:idUpdate any of the body fields; sending script_ids replaces the release's scripts in the given order
DELETE/releases/:idDelete (admin)
POST/releases/:id/signoffRecord a release-level sign-off (Approver and above)
FieldTypeRequiredNotes
namestringYesUp to 255 characters
versionstringNoUp to 50 characters; generated as vYYYYMMDD-HHMMSS (UTC) when omitted
descriptionstringNo
target_environmentstringNoEnvironment name, up to 50 characters
target_groupingstringNoGrouping or connection name, up to 100 characters
scheduled_execution_dateISO 8601 date-timeNoPlanned date
script_idsuuid[]NoExisting 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.

MethodEndpointDescription
GET/connectionsList
GET/connections/importableIDE connections eligible for import
POST/connections/importImport selected IDE connections
GET/connections/:idSingle (masked)
GET/connections/environment/:envFilter by environment
GET/connections/grouping/:groupingFilter by grouping
POST / PUT / DELETE/connections · /connections/:idCRUD; a type other than the saved engine is 400 ENGINE_CHANGE
POST/connections/:id/testConnectivity probe
GET/connections/:id/databasesThe 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" }
StatusCodeMeaning
404CONNECTION_NOT_FOUNDNo such connection, or a malformed id
502CONNECTION_FAILEDThe instance could not be reached or listed; the message gives the reason
500DATABASES_UNREADABLEAny 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.

MethodEndpointDescription
GET/executionsFull history: status, timing, rows, output, error, and what ran (executed_sql, sql_sha256, script_revision, run_mode, target_database, rollback_sql)
GET/executions/recentMost recent runs; limit from 1 to 100 (default 10), anything else is 400
GET/executions/:idSingle 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/activeThe 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/runExecute 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-releaseRun 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/rollbackRoll 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.

MethodEndpointDescription
GET/approvalsList requests
GET/approvals/policyThe 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/:idSingle request with signatures, and approved_at, or rejected_by, rejected_at and rejection_reason once decided
GET/approvals/status/:statusFilter by status: pending, approved or rejected, in any case; anything else is 400
GET/approvals/:id/blast-radiusBlast-radius report bound to the request
GET/approvals/:id/blast-radius/narrativeAI narration (needs a configured AI provider)
GET/approvals/:id/shadow-runsShadow runs for the request
GET/approvals/:id/snapshotsDecision snapshots of the request, newest first
POST/approvals/:id/snapshotsCapture a decision snapshot now (approver); returns it (201)
POST/approvalsOpen 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/signAdd 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/rejectReject 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/revokeRevoke 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/:emailWithdraw a signature while the request is pending (your own, or anyone's as an admin)
DELETE/approvals/:idWithdraw 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.

MethodEndpointDescription
GET/environmentsList: id, name, requiresApproval, promoteFrom (the environment a release must complete a run in first, or null), groupings, approvers
POST/environmentsCreate 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/:idChange 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/:id409 while another environment runs only after it, or while any connection or script uses it; the message names what is in the way
GET/groupingsList: id, name, description
POST/groupingsCreate with name and optional description; 201, or 409 for a name in use
PUT/groupings/:idRename; every connection, environment and release that used the old name is updated (connectionsUpdated)
DELETE/groupings/:id409 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.

MethodEndpointDescription
GET/auditPaginated log, newest first (?limit=&offset=, default 100)
GET/audit/searchFiltered 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/facetsThe accounts and actions recorded, { "users": [], "actions": [] }
GET/audit/user/:emailBy actor
GET/audit/action/:actionBy action
GET/audit/entity/:type/:idBy 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/integrityVerify the hash chain over the entire trail
GET/audit/export?format=csv|jsonEvery 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/streamThe 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/streamAdmin 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/streamAdmin only. Turns the stream off
POST/audit/stream/testAdmin only. Sends one AUDIT_STREAM_TEST event; 502 with the receiver's answer when it is not delivered
DELETE/audit/cleanup?days=90Removes 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.

MethodEndpointDescription
GET/settingsAll settings
GET / PUT / DELETE/settings/:keyRead / upsert / remove a single key
POST/settings/batchUpsert many keys atomically
GET/settings/policy/securityAdmin. { "values": {...}, "ranges": {...} }: the Session & Security values in effect, defaults included, and the range each number accepts
GET/settings/policy/mailAdmin. { "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.

MethodEndpointDescription
GET / POST / PUT / DELETE/discovery/hosts · /discovery/hosts/:idHost 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/scanScan a host for databases
GET/discovery/hosts/:id/historyScan history
GET/discovery/databases · /discovery/databases/:idDiscovered databases
PUT/discovery/databases/:id/statusUpdate inventory status
POST / DELETE/discovery/databases/:id/tags · /tags/:tagIdTag assignment
POST/discovery/databases/:id/connectionAdds 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/tagsTag catalog
GET / POST / PUT / DELETE/discovery/aws-accounts · /discovery/aws-accounts/:idAWS 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/syncReads 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" }] } }

Notifications — /api/notifications

MethodEndpointDescription
GET/notifications?limit=&offset=The current user's notifications, newest first; limit up to 100 (default 50)
GET/notifications/unread-countUnread badge count: { count }
POST/notifications/:id/read · /:id/unreadMark one notification read or unread
POST/notifications/read-allMark every notification read: { markedAsRead }
DELETE/notifications/:idDismiss one notification
DELETE/notifications/readClear every read notification: { deleted }
GET / PUT/notifications/preferencesThe 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.

MethodEndpointDescription
GET/data-protection/export/:userIdExport a user's data (portability)
GET/data-protection/access/:userIdAccess report
DELETE/data-protection/erase/:userIdErasure

IDE routes (any active license)

The interactive IDE calls the connection/query/schema families directly. Connections are shared with DB DevOps.

MethodEndpointDescription
GET / POST/api/connectionsList / create (backend tests reachability, then encrypts password, before persisting)
GET / PUT / DELETE/api/connections/:idRead (masked) / update / delete. A type other than the saved engine is 400 ENGINE_CHANGE
POST/api/connections/testProbe without persisting
POST/api/connections/:id/connect · /disconnectOpen / 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/databasesThe 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/executeRun a query
POST/api/queries/execute-batch · /run-scriptMulti-statement / script execution
POST/api/queries/previewHow 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/schemaSchema introspection
GET/api/databases/:connectionId/object-definitionCurrent definition of one object (below)
POST/api/databases/:connectionId/planThe 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/rowsEvery 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-scriptInsert 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/columnsObject 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" }
StatusCodeMeaning
400UNSUPPORTED_OBJECT_TYPEThe engine cannot describe that type; the message lists the types it can
400DEFINITIONS_UNSUPPORTEDThe engine has no object definitions (MongoDB, Redis, Cassandra, Neo4j, Elasticsearch)
400VALIDATION_ERRORNo object name
404CONNECTION_NOT_FOUNDNo such connection, or a malformed id
500DEFINITION_UNREADABLEThe 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).