Synadia Insights
API
Insights exposes a programmatic API over NATS request/reply using NATS micro. The API starts automatically whenever a sink connection is available and provides the same query capabilities that power the web UI.
Any NATS client connected to the same server (or cluster) can issue requests to these endpoints. This enables automation, integration with external tooling, and headless operation without the web UI.
Service Info
The API registers as a NATS micro service named insights with version 0.0.1. You can discover it using the standard micro service discovery subjects ($SRV.INFO, $SRV.PING, etc.).
Subject Hierarchy
All API subjects use the $INS prefix and follow a $INS.{domain}.{action} convention:
| Domain | Subject | Description |
|---|---|---|
db | $INS.db.query | SQL query execution |
db | $INS.db.query.stream | Streaming SQL query for large results |
db | $INS.db.explain | Validate/plan SQL without executing it |
db | $INS.db.schemas | List schemas |
db | $INS.db.tables | List tables and views in a schema |
db | $INS.db.columns | List columns of a table or view |
db | $INS.db.macros | List macros in a schema |
db | $INS.db.backup | Database backup |
ops | $INS.ops.info | Instance capability and config discovery |
ops | $INS.ops.storage | Database storage and connection-pool stats |
checks | $INS.checks.list | List audit checks |
checks | $INS.checks.info | Full metadata for one check |
checks | $INS.checks.findings | Run a check and return rows |
The first token after $INS identifies the domain, the second the action. That makes subjects predictable and easy to authorize with NATS subject permissions (for example, grant $INS.db.> for all database operations).
Versioning
The API doesn't currently encode a version in the subject hierarchy. When breaking changes happen, consumers should include an Insights-Api-Version header (for example, Insights-Api-Version: 2). Requests without the header get current behavior.
HTTP Gateway
For clients that can't speak NATS (for example, Grafana's Infinity data source), the insights http subcommand runs a stateless HTTP-to-NATS bridge. It translates each HTTP request into the matching read-only $INS.db.* NATS request and returns the reply. The gateway owns no database and runs no queries itself — the read-only guard and row caps stay enforced by the $INS.db.* handlers.
| Method & path | NATS subject |
|---|---|
POST /db/query | $INS.db.query |
POST /db/explain | $INS.db.explain |
POST /db/schemas | $INS.db.schemas |
POST /db/tables | $INS.db.tables |
POST /db/columns | $INS.db.columns |
POST /db/macros | $INS.db.macros |
GET /healthz | — (liveness, unauthenticated) |
Every route except /healthz requires a bearer token (Authorization: Bearer <token>), set with --auth-token. The gateway refuses to start without a token unless --allow-unauthenticated is given. See the CLI reference for the full flag set.
Endpoints
Query: $INS.db.query
Execute arbitrary read-only SQL queries against the DuckDB database.
Request:
{
"sql": "SELECT * FROM hx.server_ident LIMIT 10",
"params": []
}
| Field | Type | Required | Description |
|---|---|---|---|
sql | string | yes | SQL query to execute. Must be read-only (SELECT, WITH, EXPLAIN) |
params | array | no | Positional parameters for parameterized queries |
Response (JSON, default):
[
{ "name": "server-1", "cluster": "us-east", "version": "2.10.0" },
{ "name": "server-2", "cluster": "us-west", "version": "2.10.0" }
]
An array of objects, one per row, keyed by column name.
Response (CSV):
Set the Accept: text/csv header to receive results as CSV.
This endpoint buffers the whole result before responding, so it caps the row count (db.query-max-rows, default 100000). A result that would exceed the cap is rejected with 413 — add a LIMIT, or use the streaming endpoint ($INS.db.query.stream) below, which streams the result instead of buffering it.
Errors:
| Code | Condition |
|---|---|
400 | Invalid JSON, missing SQL, or non-read-only query |
413 | Result exceeds db.query-max-rows; add a LIMIT or use $INS.db.query.stream |
500 | Internal server error |
Example using nats CLI:
nats req '$INS.db.query' '{"sql": "SELECT count(*) as n FROM hx.server_ident"}'
Streaming Query: $INS.db.query.stream
Streams a large read-only result instead of buffering it. The result is delivered as ordered Apache Arrow chunks so neither the server nor the client holds the whole result in memory — the path to use for large exports. The same read-only rules as $INS.db.query apply, and it is not subject to the db.query-max-rows buffering cap (a large safety limit, db.query-stream.max-rows, default 1000000, still aborts a runaway unscoped scan).
The insights query command uses this endpoint automatically, falling back to $INS.db.query when no streaming responder is available.
Explain & Schema Discovery: $INS.db.explain / .schemas / .tables / .columns / .macros
These read-only endpoints back the insights db subcommands and the matching HTTP gateway routes. Discovery reads catalog metadata only — no data is scanned — and is scoped to the allowlisted hx, main, and audit schemas.
$INS.db.explain— plan a read-only query without executing it. Request{ "sql": "…", "analyze": false }; set"analyze": trueto runEXPLAIN ANALYZE(which executes the query, still read-only-guarded) and add per-operator runtime timing. Returns the DuckDB plan text.$INS.db.schemas— list schemas with descriptions and object counts. Empty request ({}).$INS.db.tables— list tables and views. Request{ "schema": "hx" }; omitschemato list across all allowlisted schemas.$INS.db.columns— list columns of a table or view. Request{ "schema": "hx", "table": "servers" }.$INS.db.macros— list macros with signatures. Request{ "schema": "audit" }; omitschemafor all.
Checks List: $INS.checks.list
Returns all audit checks grouped by category, including configurable parameters.
Request: Empty payload.
Response:
[
{
"label": "Server Health",
"checks": [
{
"code": "SERVER_003",
"name": "High CPU Usage",
"description": "Server CPU usage exceeds threshold",
"scope": "server",
"optimization": false,
"params": [
{
"name": "cpu_percent",
"default": 90.0,
"resolved": 90.0,
"description": "CPU usage threshold percentage"
}
]
}
]
}
]
Checks Info: $INS.checks.info
Return full metadata for a single check, including its description, severity, scope, and configurable parameters with default and resolved (currently in-effect) values.
Request:
{ "code": "SERVER_003" }
| Field | Type | Required | Description |
|---|---|---|---|
code | string | yes | Check code (e.g., SERVER_003) |
Response:
{
"code": "SERVER_003",
"name": "High CPU Usage",
"description": "Server CPU usage exceeds threshold",
"scope": "server",
"severity": "warning",
"category": "Performance",
"optimization": false,
"params": [
{
"name": "cpu_percent",
"default": 90.0,
"resolved": 90.0,
"description": "CPU usage threshold percentage"
}
]
}
Errors:
| Code | Condition |
|---|---|
400 | Missing check code |
404 | Unknown check code |
500 | Internal server error |
Checks Findings: $INS.checks.findings
Execute a specific check by code and return matching results.
Request:
{
"code": "SERVER_003",
"time": {
"duration": "1h"
},
"page": 1
}
| Field | Type | Required | Description |
|---|---|---|---|
code | string | yes | Check code (e.g., SERVER_003) |
time | object | no | Time parameters with duration |
page | int | no | Page number for pagination |
Response (JSON, default):
{
"rows": [
{
"code": "SERVER_003",
"severity": "warning",
"entity": "server-1",
"entity_pk": "NABC123",
"check_name": "High CPU Usage",
"cpu_percent": 92.3
}
],
"page_info": {
"page": 1,
"total_pages": 1,
"total_rows": 2
}
}
Response (CSV): Set Accept: text/csv header.
Errors:
| Code | Condition |
|---|---|
400 | Missing check code or invalid duration |
404 | Unknown check code |
500 | Internal server error |
Backup: $INS.db.backup
Create a transactionally consistent DuckDB backup. The backup captures all tables in the hx schema, pinned to a consistent epoch to avoid partial data from concurrent indexing.
Request:
{
"start_epoch": "2024-01-01T00:00:00Z",
"end_epoch": "2024-01-02T00:00:00Z",
"upload": true,
"retain": false
}
| Field | Type | Required | Description |
|---|---|---|---|
start_epoch | string (RFC 3339) | no | Start of epoch range to include. Omit for full backup |
end_epoch | string (RFC 3339) | no | End of epoch range to include. Omit for full backup |
upload | bool | no | Upload the backup to a NATS object store (insights-backups bucket) |
retain | bool | no | When upload is true, keep the local file after uploading. Default removes it |
All fields are optional. With an empty payload {}, a full backup is created locally.
Response:
{
"filename": "insights-backup-20240115-093045.db",
"size": 52428800,
"duration": "1.234s",
"tables": 24,
"object_name": "insights-backup-20240115-093045.db",
"bucket": "insights-backups"
}
| Field | Description |
|---|---|
filename | Local filename (empty when uploaded without retain) |
size | Backup file size in bytes |
duration | Time taken to create the backup |
tables | Number of tables backed up |
object_name | Object store key (present when upload is true) |
bucket | Object store bucket name (present when upload is true) |
Only one backup can run at a time. A concurrent request returns a 409 error.
Errors:
| Code | Condition |
|---|---|
400 | Invalid request JSON |
409 | Backup already in progress |
500 | Internal server error |
The object store TTL is controlled by the backup.object-store-ttl configuration option.
Instance Info: $INS.ops.info
Report the backend instance's capabilities and configuration. A web-only tier uses this to discover the settings of the backend it connects to, rather than reading its own local config. This is a request/reply endpoint with an empty request payload.
Response:
{
"geo": { "enabled": true },
"realtime": { "enabled": false },
"scraper": { "interval": 60000000000 },
"db": { "retention": 2764800000000000 },
"license": {}
}
| Field | Description |
|---|---|
geo.enabled | Whether IP geolocation enrichment is configured |
realtime.enabled | Whether the realtime advisory feed is available |
scraper.interval | Configured scrape cadence |
db.retention | Data-retention duration (0 disables retention) |
license | Parsed license claims (omitted when unlicensed) |
scraper.interval and db.retention are durations serialized as integer nanoseconds — 60000000000 is 1m, 2764800000000000 is 768h.
The related $INS.ops.storage subject reports the database owner's storage figures (file and WAL size) and connection-pool statistics.
Error Handling
All endpoints return errors using the NATS micro error format:
- Code. A string error code (HTTP-style:
400,404,500, and so on). - Description. A human-readable error message.
- Data. Optional additional context (JSON bytes).
Internal errors get masked with a generic 500 / "internal server error" response, so implementation details don't leak.
Content Negotiation
The db.query and checks.findings endpoints support content negotiation via the NATS message Accept header:
application/json(default). Results as a JSON array/object.text/csv. Results as CSV with a header row.