Skip to content

Serve / SQL interface

SQL interface ​

Beta (v1.1 feature flag)

The SQL interface ships behind the tenant feature flag sql-interface, off by default. An Owner turns it on in Settings › Serving › Connect with SQL (or PUT /api/v1/tenant/features/sql-interface with {"enabled": true}).

Tools that speak PostgreSQL — Tableau, Power BI, Metabase, DBeaver, Excel over ODBC, psql, Npgsql, JDBC, psycopg — can query a serving endpoint as tables. Every query becomes a semantic query on the same governed path as the HTTP API: the endpoint's release, exposure, API-key allow-list, on/off switch and rate limit, the caller's access policies (masks, row filters, deny), the result cache and the query log. No SQL is ever passed to your database.

Connect ​

SettingValue
Host / portthe SQL gateway, port 5433 (locally localhost:5433)
Databasekeel
Userthe serving endpoint's slug, e.g. sales
Passwordan API key with the serving:query scope (keel_live_…)
SSLrequire — the password is only accepted over TLS (local development also accepts unencrypted connections)

Create the key in Settings › API keys (and allow it on the endpoint if the endpoint has a key allow-list). API keys are evaluated as Analysts by access policies.

bash
psql "host=localhost port=5433 dbname=keel user=sales sslmode=require"   # prompts for the API key

The local development stack runs the gateway without TLS, so use sslmode=disable there (the command shown in Settings › Serving uses require, as production does). psql's \d / \dt meta-commands are not supported yet: they use OPERATOR(pg_catalog.~) syntax. Browse with information_schema instead, for example SELECT table_name FROM information_schema.tables WHERE table_schema = 'keel'.

  • Npgsql: Host=localhost;Port=5433;Database=keel;Username=sales;Password=keel_live_…;SSL Mode=Require
  • JDBC: jdbc:postgresql://localhost:5433/keel?user=sales&password=keel_live_…&sslmode=require
  • BI tools: choose the PostgreSQL connector with the values above.

Tables ​

Schema keel holds, for the endpoint's release:

  • one table per entity that has metrics — its metrics plus every exposed dimension they can be grouped by;
  • metrics — every exposed metric and dimension.

Dimension columns are named entity_dimension (customer_status, order_date_calendar_date); the quoted semantic name ("customer.status") works too. Metrics are numeric; dimensions are text, numeric, boolean or timestamp. Business names appear as table and column comments. Metrics and dimensions your policies deny are simply not there, and masked values arrive masked (****, a hash, or NULL in a numeric or time column, with a notice).

Queries ​

sql
SELECT customer_status, SUM(revenue) AS revenue, order_count
  FROM keel.orders
 WHERE order_date_calendar_date >= '2026-01-01'
   AND customer_status IN ('active', 'new')
 GROUP BY customer_status
 ORDER BY revenue DESC
 LIMIT 10;

SELECT date_trunc('month', order_date_calendar_date) AS month, MEASURE(net_revenue)
  FROM keel.orders
 GROUP BY 1
 ORDER BY 1;
ClauseSupported
SELECTdimension columns; metric columns, plain or in SUM / MIN / MAX / AVG / MEASURE / AGG / ANY_VALUE (the model's own aggregation always applies); date_trunc('day'…'year', time_column); aliases
FROMexactly one keel table (keel.orders, orders, with an alias)
WHEREdimension conditions joined with AND: = <> != < <= > >=, IN, NOT IN, BETWEEN, IS [NOT] NULL, LIKE '%text%' (contains), LIKE 'text', a = x OR a = y, NOT before those; constants, casts, DATE '…', $1 parameters
GROUP BYdimensions by name, position or alias
ORDER BYselected fields by name, position, alias or aggregate
LIMIT / OFFSET / FETCH FIRSTyes; without LIMIT at most 10,000 rows (a notice says when a result was cut)

Anything else is refused with SQLSTATE 0A000 and a hint: joins, subqueries, HAVING, conditions on metrics, COUNT(*) (use a count metric), other functions and expressions, window functions, WITH, and SELECT * except in probes such as SELECT * FROM keel.orders WHERE 1 = 0. Writes get 25006 (read-only).

SQLSTATEMeaning
28P01the password is not a valid API key
28000unknown or disabled endpoint, key without serving:query, or no encrypted connection
0A000outside the supported SQL, or the SQL interface is off for the tenant
42P01 / 42703unknown table / unknown or hidden column
42803a selected dimension missing from GROUP BY, or a metric in it
42501the flag was turned off, or the key was denied, during the session
53400the endpoint's rate limit

Trust and compiler warnings (a deprecated metric, a low trust score) arrive as notices (WARNING). Every query is in the query log as consumer sql with the client's application_name, and shows up in lineage impact analysis.

What tools can browse ​

SELECT version(), current_database(), current_schema(), current_user, current_setting(…); pg_catalog (pg_namespace, pg_class, pg_attribute, pg_type, pg_description, pg_database, pg_roles, pg_settings, pg_tables, …) and information_schema (schemata, tables, columns, table_privileges) with joins, subqueries, CASE, ::regclass, GROUP BY, row_number() OVER (…) and UNION. SET, SHOW, BEGIN / COMMIT / ROLLBACK, DISCARD, DEALLOCATE and CLOSE are accepted. Tested with Npgsql (connect, GetSchema, parameters, prepared statements) and JDBC-style metadata queries; if your tool sends something the gateway doesn't answer, the error names it.

Operating it

  • TLS: configure Keel:SqlGateway:CertificatePath (PKCS#12) and CertificatePassword, or terminate TLS in a TCP proxy in front of the gateway and set Keel:SqlGateway:AllowCleartextPasswordWithoutTls. Passwords are never accepted unencrypted otherwise. SCRAM authentication is not supported (API keys are stored hashed; ADR-0018).
  • Cancel requests are best effort: behind a load balancer they may reach another instance and be ignored.

Anchor Keel is a working product name: one platform, a shared core and licensed modules (Keel Semantic, Keel Migrate). Documentation built from the repository — facts are derived from code, ADRs and the decisions log.