Tier 1 types get every feature, are tested against a real server in CI and sit under the compatibility promise. Experimental types work, with documented gaps. The same guard, pooling, parameter binding and output formats apply to every one; what each supports is in the support matrix.
The support matrix →Trino Exp: one connection reaches every catalog behind a Trino endpoint, Hive, Iceberg and Delta tables included, with the same guard, masking and limits. Guide 61 →
Idle connections kept per connection, QUERYAPIGATE_POOL_SIZE, and a query time limit enforced on every front door (on every database type but generic JDBC), lowered per key or per request with ?timeout.
PostgreSQL's sslmode and MySQL's ssl_* settings are passed to the driver: a connection that asks for a verified certificate fails rather than connecting without one.
Passwords as ${VAR} references or encrypted at rest under a key you can rotate; or short-lived credentials from Vault or RDS IAM Exp.
Point a connection at a folder or bucket of Parquet, CSV or JSON and each file becomes a view through DuckDB, locked to the paths that connection allows.
Built to expose specific query results, not database credentials. A key identifies an application; every grant below narrows what it reaches, and writes are off by default. Hand an external client a key that can only ever reach one named query while internal callers keep connection-level access.
The eight stages every request passes, step by step →connectionsEvery saved query on it, plus ad-hoc SQL if allowed.
queriescollectionsName the saved queries a key may run, one at a time or as a collection. Nothing else answers.
allowed_tablesAd-hoc SQL confined to named tables; the SQL is parsed, not pattern-matched.
maskHash, mask, null or drop named source columns. Traced through aliases, CTEs and joins; refused rather than served unmasked.
allowed_write_opsWrites off by default. Turn on only INSERT, or UPDATE on one query.
rate_limitallowed_ipsA per-key request budget on top of the server-wide, IP-keyed limit (a request has to pass both), and a list of IPs or CIDR ranges the key may call from.
expires_atlast_used_atTime-boxed keys for trials and contractors; alerts before expiry and for keys gone unused.
Authorization: BearerAccept the JWT your app already has (Auth0, Cognito, Keycloak, Azure AD or your own secret). Each user calls as themselves, mapped to a role.
max_concurrentquery_timeoutmax_rowsCap the queries a key runs at once, how long each may run and the rows a call returns, across every instance. Over it: 429 too_many_concurrent.
quotaCalls and rows per day or month, with X-Quota-Remaining-* headers on every response and an alert before it runs out.
rotateReissue a key's secret while the old one keeps working for a grace period, or zero for a leaked one.
owner · admin · developer · auditorThe people and pipelines that manage the server are named, with the narrowest role that fits, so every change in the audit log has a person behind it.
Single sign-on and the break-glass key →API keys are stored as SHA-256 hashes, never the raw secret. Database passwords are encrypted under QUERYAPIGATE_SECRET_KEY and decrypted only in memory when a connection opens; a literal password on disk is never returned to a client either way.
Restrict a key to a set of connections, to an allow-list of saved queries independent of any connection grant, or both. A query in that list can be curated for write access without granting it anywhere else the key reaches.
Guide 1 →A reusable template of connections, write access, queries, collections, rate limit and allowed IPs, copied onto a key once at creation. Editing or deleting a role afterwards never affects a key already created from it.
File saved queries into named groups and grant a key a whole group, live. Every move that changes who can reach a query is audited with the keys that gained or lost access. Rename without any key losing reach; export a bundle and import it elsewhere.
Guide 7 →Several teams on one server, each with its own connections, saved queries, keys, roles, destinations and exports, and its own history, audit entries, alerts and events. Every route also answers at /w/<workspace>/…; a key works in its own, and a workspace's owner manages its members. ADR 0023 →
Only single SELECT, WITH, SHOW, DESCRIBE and EXPLAIN statements pass by default, with bound :name parameters rather than string-concatenated SQL. Ad-hoc SQL, where a key is allowed it, is confined to allowed_tables and recorded like any saved query.
The same saved queries are tools an AI agent can call over the Model Context Protocol, served by a dedicated process, queryapigate mcp. It is the same governed core as REST: an agent gets the same identity, grants, column masking, limits and audit as any app, enforced in the gateway and never asked of the model.
What an agent sees in tools/list: the parameters, their types, bounds and an example, and the tables the query reads, so each argument means something rather than being just a name.
Each published query is an MCP tool carrying its parameters, their types, descriptions and an example, and the tables it reads. A query can be hidden from MCP with mcp: false while it keeps serving over REST.
The ad-hoc execute_sql, list_tables and describe_table tools are forced read-only whatever the key's own write grant. An agent's writes are held as requests and run only once a person approves them, with exactly the arguments shown.
describe_table and schema discovery honour column masking: a masked or dropped column is flagged, never shown as readable, so an agent cannot even learn that hidden data exists.
The MCP server is an OAuth 2.0 protected resource (RFC 9728). An agent signs in through your identity provider, and the token's audience is bound to the MCP server's own URL, so it cannot be replayed against REST or the database.
MCP runs land in the same run history, per-key metrics and tamper-evident audit log as every other caller.
Claude Desktop, Cursor, LangChain or any MCP client: point it at the server, sign in, and the published queries appear as tools.
Guide 24 →Semantic search Exp: a saved query takes an embed parameter, and the agent's words become a vector for pgvector to rank. Search by meaning →
A saved query bundles SQL, name, description, parameters, author, version, permissions and optional cache settings into one addressable resource, GET /q/<name>. Saving again under the same name creates a new version rather than overwriting the old one, so a change to the SQL never silently changes what is already live; ?version=1 still runs the prior one, and every run is recorded in the query's history.
Each saved query is versioned, has typed parameters with ranges and lengths, and keeps a run history. OpenAPI and a Postman collection are generated, and OpenAPI Generator turns the spec into a typed client in about 50 languages. Group queries into collections and grant access to the whole collection at once.
Every saved query has at most one published version: the one /q/<name> runs and the one OpenAPI, MCP and Postman describe. Newer versions are drafts, invisible to callers until published; the admin key can test one by number. Roll back to any earlier version. Older versions stay runnable with ?version=.
SELECT * FROM film WHERE film_id = :id answers GET /q/film_by_id?id=7. Values travel to the database separately from the SQL text, so they cannot be injected into it. A parameter can declare a type, default, required or optional, enum, numeric range, length and pattern; invalid input is rejected with a field-by-field 400 before it reaches the database.
Each :name in the SQL is a bound parameter with a type, bounds, length, enum, pattern and default. A request that breaks a rule gets a 400 listing every problem and never reaches the database. from_claim takes a value from the caller's sign-in token instead of the request, so a user can only ever ask for their own rows.
JSON, NDJSON, XML, YAML, CSV, TSV, XLSX and Parquet are all available per request with ?format=, so the same saved query serves application clients, reporting and data pipelines. A Parquet export keeps column types and is typically a quarter of the CSV's size.
?page=2&page_size=50 pages results, with X-Has-More telling the caller whether more exist. For full exports, ?stream=true with CSV, TSV or NDJSON streams the entire result straight from the database cursor rather than buffering it, verified end to end with 1,000,000-row results on MySQL, PostgreSQL and ClickHouse.
?page= and ?page_size= for typical results, with X-Has-More and X-Row-Count headers so a client knows when to stop.
Constant memory however large the result, in CSV, TSV or NDJSON, with a row ceiling you can set server-wide (QUERYAPIGATE_STREAM_MAX_ROWS, unbounded by default).
queryapigate export writes a full result to a file through the same streaming path, with no server running. Atomic rename on completion, non-zero exit on failure, deliveries to S3, GCS, R2 or disk that resume where the last run stopped.
A saved query can set cache_ttl and get Cache-Control, ETag, conditional requests and 304 Not Modified, plus an X-Cache: HIT or MISS header on every response. Never applied to a query that writes.
A per-query cache_ttl sets Cache-Control and ETag, answers conditional requests with 304 and reports X-Cache: HIT or MISS; never on a query that writes. In-process by default; with Redis, one cache shared by every instance, surviving restarts.
ETag and 304 Not Modified on cached responses, If-Match for safe concurrent edits, X-Has-More and X-Row-Count for paging, and ?timeout per request.
A server-wide, IP-keyed limit and an independent, optional per-key limit can both be in effect at once; a request has to pass both. Over a limit the answer is a 429 with the budget left in its headers.
QUERYAPIGATE_RATE_LIMIT, no per-key limit.QUERYAPIGATE_RATE_LIMIT keyed by caller IP, enforced on every front door. With Redis, one budget across every instance; if Redis blinks, limits stay enforced per process.
rate_limit on the key, plus allowed_ips to pin it to addresses or CIDR ranges and expires_at to end it on a date.
max_concurrent, query_timeout and max_rows cap what one key can hold at once; quota counts calls and rows per day or month, with X-Quota-Remaining-* headers and an alert before it runs out.
Structured JSON logs tagged with a request ID, per-query execution timing, slow-query warnings over a threshold you set, and Prometheus metrics at /metrics covering request and query counts and latencies, connection-pool occupancy and rate-limit rejections. Run history is per query and per key; the audit log records who changed what.
Also watched: break_glass_used, key_expired, query_timeouts, key_rate_limited, client_rate_limited, history_failed, history_dropped, instances_not_shared, instances_versions_differ, rate_limits_not_shared, encryption_key_rotation_incomplete. 25 kinds in all.
Shown on the Console's Alerts screen, the bell in its header, and Home's System health. For paging, scrape /metrics with Prometheus.
Structured JSON logs with request IDs, slow-query warnings over a threshold you set, per-key metrics, a Prometheus /metrics endpoint behind a token, a bundled Grafana dashboard, and OpenTelemetry spans Exp from the request to the database call, never holding a value or a row.
A SQLite file by default, or PostgreSQL for several instances. Versioned schema with upgrades tested in CI, backup and restore, passwords encrypted at rest.
Redis, optional: one cache, one rate-limit budget and one event stream across every instance. If Redis blinks, limits stay enforced per process.
pip, the image on ghcr (with an -h2 variant including Java), a Helm chart for one pod or several, and a multi-instance Compose setup behind Caddy. Non-root, read-only root filesystem.
Passwords encrypted at rest under a key you can rotate, or no password at all: ${file:…}, ${vault:…} and ${aws-sm:…} references, short-lived credentials from Vault or RDS IAM. AWS paths experimental.
config export, config diff and config apply: the whole setup as one YAML file with no secret in it, reviewed in a pull request and applied all or nothing, by CI or at startup.
Locked, if you want it. With QUERYAPIGATE_CONFIG_LOCKED, what the file manages can't be changed in the Console or the API, only through the file.
Drift, shown. Without the lock, a change made by hand on prod shows in config diff, and the next apply undoes it.
GitOps. On Kubernetes, the Helm chart's configAsCode applies the file as the first new pod starts: Argo CD or Flux syncing the chart is the deploy.
/openapi.json is a complete, valid OpenAPI 3.0 document, checked in CI against the official validator, with every saved query listed as its own typed endpoint; /docs serves it through Swagger UI. A key's endpoint list is filtered to what that key can actually reach, so a query-scoped external key sees only its approved queries. Every error has the same shape, and every response carries a request ID.
/docs lists every saved query as its own endpoint with typed parameters; /catalog tells each caller what it can call and on what terms: cached, writable, its rate limit. /openapi.json is checked in CI against the official validator, and a key sees only the endpoints it can reach.
OpenAPI 3.0, validated in CI, turned into a client in about 50 languages by OpenAPI Generator; a Postman collection per collection.
See it on Home →Every error has the same shape: error, a code and a request_id. Codes are part of the compatibility promise: an existing one is never renamed or repurposed.
The Management API, /api/v1, does everything the Console does: connections, queries, keys, roles, administrators, audit. Or keep the whole setup in one YAML file.
Group queries, grant them together, rename, export and import a collection between servers, and hand out its Postman collection.
Guide 7 →QUERYAPIGATE_CORS_ORIGINS for the sites you control, with your users' own tokens rather than a shared key in the page.
queryapigate examples load: four worked APIs on a generated database, with collections, roles and a scoped key for each.
Server-sent events as queries run, per key if you want. queryapigate events reads every instance's runs from the shared store, woken by a PostgreSQL notification.
A built-in UI at /console covers the whole workflow: connection management, a SQL editor with syntax highlighting and schema browsing, query execution and preview, EXPLAIN, saved-query and version management, API keys, the audit log, per-query run history, a collapsible JSON tree for non-tabular results, a quick bar chart of any numeric result, and copy as curl or TSV for any result.
Manage connections, saved queries, API keys and roles. Run ad-hoc SQL with a schema browser. Review the audit log and alerts, approve agents' writes, manage administrators, exports and quotas, and see who can reach what on the access map.
Served from /console, nothing extra to install. A collapsible sidebar (Ctrl+B) and a read-only Settings screen that shows every variable's effective value (secrets as set or not, never shown) with copy as .env; theme, density and default format kept per browser. Everything it does is also available on the Management API.
Which keys can call this query, and what this key can call, from the grants as they add up. Moving a query between collections is audited with the keys that gained or lost access.
Guide 21 →30-day activity on every access screen (keys, connections, cached queries, MCP tools, roles) and a details screen per key: what's busy, and what's granted but never touched.
Guide 62 →Unit tests; integration tests against real PostgreSQL, MySQL, ClickHouse, H2 and MongoDB servers; property tests and fuzzing of the SQL guard; CodeQL, mypy and ruff; CI on every push across Python 3.11 to 3.14. Audit, cryptography and release signing are on the Enterprise page, data handling in the Trust center.
The signed audit log and its copy in your SIEM, FIPS-approved cryptography, and releases you can verify before they run.
Enterprise →Every place data is kept or sent, for how long and who reads it. Query strings kept out of the access log. history forget erases one person's runs on request.
Security headers on every response from all three servers, a pinned Swagger UI with Subresource Integrity, no version banner.
Security →Integration tests run in CI against PostgreSQL, MySQL, ClickHouse, H2 and MongoDB service containers, and the job fails if one of them skips. DuckDB's run unconditionally, since it is embedded.
The SQL guard is property-tested with Hypothesis and fuzzed with Atheris under ClusterFuzzLite; CodeQL scans every push.
The fuzzing setup →mypy and ruff, on Python 3.11 to 3.14, at the oldest dependency versions allowed, on a PostgreSQL store and under FIPS.
Every call passes the same eight stages, whichever front door it came through. Home follows one request through them, with the refusals a bad key or parameter would get at each step.
Follow a request on Home →an API key, an admin token, a signed-in user's JWT or an OAuth token.
See this stage →types, ranges, lengths; values are bound, never spliced into the SQL.
See this stage →one statement, read-only unless allowed, only the tables a key may see.
See this stage →An optional Rust data plane on the public port serves saved-query reads from the core's own plans, and forwards everything else. Every decision stays in the core.
The data plane →