Readyset Docs

Router Admin API

REST API reference for the QueryPilot Router admin surface and its plugin endpoints.

Overview

The Router (binary querypilot) serves a REST admin API for translations, routing, configuration reload, statistics, pattern rules, and pool control, plus the Accelerator and Guard plugin endpoints.

The admin API has no bind address of its own. It listens on the [metrics] bind IP at the metrics port plus one: with the default 127.0.0.1:9090, the admin API is 127.0.0.1:9091. If the metrics port is 65535, startup fails with a port-overflow error.

The server starts when [metrics] enabled = true (full admin surface) or when an enabled plugin contributes REST routes. When only a plugin is enabled, only the plugin routes are served; enabling Guard alone does not expose the admin control plane.

The core admin routes are unauthenticated. Keep the metrics bind on loopback or a private network, and firewall both the metrics port and the derived admin port. The plugin routes support an api_key; set one whenever the bind is not loopback. See Security.

Errors return JSON with an error field. All examples below assume the default 127.0.0.1:9091.

Translations

Query translations rewrite matching queries and pin them to a pool. Translations registered through this API are ephemeral: a config reload replaces the registry with the [[translation.rules]] from the file.

POST /api/translations

Register a translation. Send concrete SQL for both the original and translated query; the Router parameterizes both and builds the index mapping. The target pool must exist in the current config.

curl -X POST http://127.0.0.1:9091/api/translations \
  -H 'Content-Type: application/json' \
  -d '{
    "original_sql": "SELECT name FROM users WHERE id = 42",
    "translated_sql": "SELECT name FROM analytics.users WHERE id = 42",
    "target_pool": "analytics"
  }'
{
  "hash": 1234567890123456789,
  "normalized": "SELECT name FROM users WHERE id = $1",
  "original_values": 1,
  "translated_values": 1
}

GET /api/translations

List all registered translations.

curl http://127.0.0.1:9091/api/translations
{
  "translations": [
    {
      "hash": 1234567890123456789,
      "original_sql": "SELECT name FROM users WHERE id = $1",
      "translated_pattern": "SELECT name FROM analytics.users WHERE id = $1",
      "target_pool": "analytics",
      "enabled": true
    }
  ],
  "total": 1,
  "enabled": 1
}

DELETE /api/translations/:hash

Delete a translation by fingerprint hash. Returns 204 No Content on success, 400 with an error body if the hash is unknown.

curl -X DELETE http://127.0.0.1:9091/api/translations/1234567890123456789

PUT /api/translations/:hash/enable

Enable a translation. Returns 204 No Content.

curl -X PUT http://127.0.0.1:9091/api/translations/1234567890123456789/enable

PUT /api/translations/:hash/disable

Disable a translation without deleting it. Returns 204 No Content.

curl -X PUT http://127.0.0.1:9091/api/translations/1234567890123456789/disable

Routing

GET /api/routing/test

Dry-run the routing decision for a SQL query without executing it. Query parameters: sql (required) and in_transaction (optional bool, default false). The response includes the query's fingerprint hash, which you can use in type = "hash" pattern rules and in the pattern-rules API below.

curl 'http://127.0.0.1:9091/api/routing/test?sql=SELECT%20*%20FROM%20orders%20WHERE%20id%20%3D%201'
{
  "decision": "pool",
  "target_pool": "primary",
  "rewritten_sql": null,
  "fingerprint_hash": 9876543210987654321,
  "normalized_sql": "SELECT * FROM orders WHERE id = $1",
  "cached": false,
  "duration_ns": 12345
}

decision is one of pool, translation (with rewritten_sql set), reject (with the rejection message in rewritten_sql), or pinned (the session is pinned mid-transaction).

Configuration

POST /api/config/reload

Re-read the config file from disk and apply the reloadable parts: routing rules, pattern rules, translation rules, read/write split settings, routing cache settings, and log level. Validation failures (bad regex, unknown pool) return 400 and leave the running config untouched.

curl -X POST http://127.0.0.1:9091/api/config/reload
{
  "success": true,
  "reloaded_at": "2026-07-18T12:00:00Z",
  "changes": {
    "routing_rules": 2,
    "pattern_rules": 1,
    "translation_rules": 3,
    "read_write_split_changed": false,
    "cache_config_changed": false,
    "log_level_changed": false
  }
}

GET /api/config/status

Report the config file path, load time, last reload attempt, and a summary of the active configuration. The shipped container healthcheck probes this endpoint.

curl http://127.0.0.1:9091/api/config/status
{
  "config_file": "/etc/sqp/querypilot.toml",
  "loaded_at": "2026-07-18T11:00:00Z",
  "current": {
    "read_write_split": true,
    "read_pool": "replica",
    "write_pool": "primary",
    "routing_rules_count": 2,
    "pattern_rules_count": 1,
    "translation_rules_count": 3,
    "cache_enabled": true,
    "log_level": "info"
  }
}

Query statistics

GET /api/stats/digests

List per-query digest statistics, ranked and filtered. Query parameters:

  • limit: maximum entries returned (default 100)
  • order_by: one of count_star, sum_rows_sent, last_seen, mean_time, query_throughput, sum_time (default sum_time); an unrecognized value returns an error
  • min_count: minimum execution count (default 0)
  • min_rows_sent: minimum total rows sent (default 0)
  • schema: exact schema filter
curl 'http://127.0.0.1:9091/api/stats/digests?order_by=count_star&limit=10'
{
  "digests": [
    {
      "digest_text": "SELECT * FROM orders WHERE id = $1",
      "fingerprint_hash": 9876543210987654321,
      "schema": "public",
      "username": "app",
      "count_star": 15230,
      "sum_time_us": 4820000,
      "sum_rows_sent": 15230,
      "first_seen": 1752830000,
      "last_seen": 1752840000
    }
  ],
  "total_unique_digests": 42,
  "overflow_count": 0
}

Pattern rules

Runtime pattern rules route by fingerprint hash. Rules created here are hash-matched, unowned by any plugin, and separate from the [[routing.pattern_rules]] in the config file.

POST /api/pattern-rules

Create a rule routing a fingerprint to a pool. Body fields: fingerprint_hash (required), target_pool (required), comment (optional). Returns 201 Created.

curl -X POST http://127.0.0.1:9091/api/pattern-rules \
  -H 'Content-Type: application/json' \
  -d '{"fingerprint_hash": 9876543210987654321, "target_pool": "replica", "comment": "hot lookup"}'
{
  "fingerprint_hash": 9876543210987654321,
  "status": "created"
}

GET /api/pattern-rules

List runtime pattern rules.

curl http://127.0.0.1:9091/api/pattern-rules
{
  "rules": [
    {
      "fingerprint_hash": 9876543210987654321,
      "target_pool": "replica",
      "enabled": true,
      "comment": "hot lookup",
      "created_at": 1752840000
    }
  ],
  "total": 1
}

DELETE /api/pattern-rules/:hash

Remove a rule. Returns 204 No Content, or 404 if the hash is unknown.

curl -X DELETE http://127.0.0.1:9091/api/pattern-rules/9876543210987654321

PUT /api/pattern-rules/:hash/disable

Disable a rule in place. Returns 200 OK, or 404 if the hash is unknown. There is no enable endpoint: to re-enable a disabled rule, delete it and create it again.

curl -X PUT http://127.0.0.1:9091/api/pattern-rules/9876543210987654321/disable

Pools

GET /api/pools

List pools and their health status.

curl http://127.0.0.1:9091/api/pools
{
  "pools": [
    { "name": "primary", "status": "online" },
    { "name": "replica", "status": "online" }
  ],
  "total": 2
}

PUT /api/pools/:name/status

Override a pool's status. Body: {"status": "..."} where the value is online, offline_soft (drain: no new assignments), or shunned (excluded). Returns 200 OK, or 503 if pool management is unavailable.

curl -X PUT http://127.0.0.1:9091/api/pools/replica/status \
  -H 'Content-Type: application/json' \
  -d '{"status": "offline_soft"}'

Accelerator plugin API

Mounted at /api/v1/plugins/accelerator when [plugins.accelerator] is enabled. Mutating endpoints require the X-QueryPilot-Api-Key header when an api_key is configured, compared in constant time. When the admin API binds a non-loopback address and no api_key is set, the mutating endpoints refuse to serve entirely (403). Every mutation is recorded in the audit trail.

GET /api/v1/plugins/accelerator/candidates

List caching candidates from the query digest, with per-candidate threshold flags. The literal-bearing digest_text is redacted unless the request presents a valid key.

curl -H 'X-QueryPilot-Api-Key: change-me' \
  http://127.0.0.1:9091/api/v1/plugins/accelerator/candidates
[
  {
    "fingerprint_hash": "0x89ABCDEF01234567",
    "schema": "public",
    "username": "app",
    "count": 15230,
    "sum_time_us": 4820000,
    "first_seen": 1752830000,
    "last_seen": 1752840000,
    "past_execution_threshold": true,
    "past_warmup_window": true,
    "digest_text": "SELECT * FROM orders WHERE id = 1"
  }
]

GET /api/v1/plugins/accelerator/rules

List routing rules owned by the Accelerator.

curl http://127.0.0.1:9091/api/v1/plugins/accelerator/rules
[
  {
    "fingerprint_hash": "0x89ABCDEF01234567",
    "target_pool": "readyset",
    "state": "active",
    "enabled": true,
    "created_at": 1752840000
  }
]

state is active, warmup:until=<unix>, health_disabled, or operator_disabled.

GET /api/v1/plugins/accelerator/status

Report discovery-loop health: last tick, duration, result, last success, consecutive errors, and pause state.

curl http://127.0.0.1:9091/api/v1/plugins/accelerator/status
{
  "last_tick_unix": 1752840000,
  "last_duration_ms": 120,
  "last_result": "ok",
  "last_success_unix": 1752840000,
  "consecutive_errors": 0,
  "paused": false
}

GET /api/v1/plugins/accelerator/decisions

List recent discovery-cycle outcomes (tick time, result, duration).

curl http://127.0.0.1:9091/api/v1/plugins/accelerator/decisions

GET /api/v1/plugins/accelerator/denylist

Report how many denylist rules are loaded. Entry text is not returned; it can contain literal-bearing SQL.

curl http://127.0.0.1:9091/api/v1/plugins/accelerator/denylist
{ "rule_count": 3 }

GET /api/v1/plugins/accelerator/audit

Read the bounded audit trail of mutations performed through this API. Requires a valid key when one is configured.

curl -H 'X-QueryPilot-Api-Key: change-me' \
  http://127.0.0.1:9091/api/v1/plugins/accelerator/audit

DELETE /api/v1/plugins/accelerator/rules/:hash

Drop an Accelerator-owned rule by fingerprint hash (decimal). Returns 204 No Content; 403 if the rule exists but is not owned by the Accelerator. Mutating: key required when configured.

curl -X DELETE -H 'X-QueryPilot-Api-Key: change-me' \
  http://127.0.0.1:9091/api/v1/plugins/accelerator/rules/9920397370294392167

POST /api/v1/plugins/accelerator/actions/force-cache

Queue a fingerprint for caching, bypassing the execution-count and warmup thresholds. The fingerprint must exist in the current digest, be SELECT-shaped, and pass the denylist; the Readyset support check still applies when the loop processes it. Returns 202 Accepted. Mutating: key required when configured.

curl -X POST -H 'X-QueryPilot-Api-Key: change-me' \
  -H 'Content-Type: application/json' \
  -d '{"fingerprint_hash": 9920397370294392167}' \
  http://127.0.0.1:9091/api/v1/plugins/accelerator/actions/force-cache
{
  "fingerprint_hash": "0x89ABCDEF01234567",
  "status": "queued"
}

POST /api/v1/plugins/accelerator/actions/pause

Pause the discovery loop. Mutating: key required when configured.

curl -X POST -H 'X-QueryPilot-Api-Key: change-me' \
  http://127.0.0.1:9091/api/v1/plugins/accelerator/actions/pause
{ "paused": true }

POST /api/v1/plugins/accelerator/actions/resume

Resume a paused discovery loop. Mutating: key required when configured.

curl -X POST -H 'X-QueryPilot-Api-Key: change-me' \
  http://127.0.0.1:9091/api/v1/plugins/accelerator/actions/resume

Guard plugin API

Mounted at /api/v1/plugins/guard when [plugins.guard] is enabled. Read endpoints are open; mutating endpoints (POST /policies/reload, POST /registry, DELETE /registry/:fingerprint) require the X-Guard-Api-Key header (or Authorization header) to match the configured api_key. When no api_key is set, all endpoints are open; that is a development posture only.

GET /api/v1/plugins/guard/policies

List loaded policies.

curl http://127.0.0.1:9091/api/v1/plugins/guard/policies
[
  {
    "name": "no-unbounded-deletes",
    "severity": "high",
    "enabled": true,
    "description": "Block DELETE without a WHERE clause"
  }
]

POST /api/v1/plugins/guard/policies/reload

Reload policies from a directory. Body: {"dir": "..."}, or an empty object {} to reload from the configured policy_dir. Mutating: key required when configured.

curl -X POST -H 'X-Guard-Api-Key: change-me' \
  -H 'Content-Type: application/json' -d '{}' \
  http://127.0.0.1:9091/api/v1/plugins/guard/policies/reload
{ "loaded": 4 }

GET /api/v1/plugins/guard/registry

List trusted-query registry entries.

curl http://127.0.0.1:9091/api/v1/plugins/guard/registry

POST /api/v1/plugins/guard/registry

Add a registry entry. The body is a registry entry object; on success returns {"ok": true}. Mutating: key required when configured.

curl -X POST -H 'X-Guard-Api-Key: change-me' \
  -H 'Content-Type: application/json' \
  -d @entry.json \
  http://127.0.0.1:9091/api/v1/plugins/guard/registry

GET /api/v1/plugins/guard/registry/:fingerprint

Fetch one registry entry by fingerprint (hex like 0xABC... or decimal). Returns 404 if absent.

curl http://127.0.0.1:9091/api/v1/plugins/guard/registry/0x89ABCDEF01234567

DELETE /api/v1/plugins/guard/registry/:fingerprint

Remove a registry entry. Returns {"removed": true}, or 404 if absent. Mutating: key required when configured.

curl -X DELETE -H 'X-Guard-Api-Key: change-me' \
  http://127.0.0.1:9091/api/v1/plugins/guard/registry/0x89ABCDEF01234567

POST /api/v1/plugins/guard/test

Dry-run a decision against the live policy set without executing the query. Body: sql (required), username (optional), trust_level (optional).

curl -X POST -H 'Content-Type: application/json' \
  -d '{"sql": "DELETE FROM orders", "trust_level": "verified"}' \
  http://127.0.0.1:9091/api/v1/plugins/guard/test
{
  "decision": "block",
  "policy": "no-unbounded-deletes",
  "message": "DELETE without WHERE is not allowed",
  "reason": "policy matched",
  "latency_us": 180
}

decision is one of allow, block, amend, or reroute.

GET /api/v1/plugins/guard/decisions

Read recent audit records from the in-memory ring buffer. Query parameter limit (default 100, capped at the buffer capacity).

curl 'http://127.0.0.1:9091/api/v1/plugins/guard/decisions?limit=20'

GET /api/v1/plugins/guard/stats

Report registry size, policy counts, and audit health.

curl http://127.0.0.1:9091/api/v1/plugins/guard/stats
{
  "registry_size": 12,
  "policies_loaded": 4,
  "policies_invalid": 0,
  "policies_compile_warnings": 0,
  "audit_dropped": 0
}