One query for every tenant, and each sees only its own
Your users call with the token they already sign in with. Its plan picks a role, with that plan's queries, rate limit and quota, counted per user. Its tenant fills the query's filter, so the request has nothing to change. The cache keeps one entry per tenant, and tenants never share one.
Stablecompatibility promiseEvery run is recorded as jwt:<user>. A cache hit isn't, because nothing ran, but it counts toward the quota.
Your productEach user calls with the token they sign in withThe page calls directly. No key sits in it.
↓
The tokenIt carries the tenant and the planVerified with your identity provider's keys before anything in it is used.
↓
The governed coreToken → plan → tenant → cacheThe plan picks a role: queries, rate limit, quota, per user. The tenant fills the query's filter, and a request that sends one gets 400. One cache entry per tenant; tenants never share one.
↕
AsksIdentity provider, RedisSigning keys fetched once; the cache and quota counts shared across instances.
↓
Your dataClickHouse or DuckDB: only the caller's tenantOne saved query, WHERE tenant_id = :tenant_id, for every tenant.
whojwt:alice@acmedoorGET /q/revenue_by_monthtenant from token ✓role from plan ✓cache per caller ✓dataClickHouse · analytics
For SaaS products
Put analytics in front of your customers, one tenant at a time
Customers want their own numbers inside your product. Building that usually means an analytics backend with tenant isolation you have to get exactly right. Here, each user signs in with the token your app already issues, and the tenant comes from that token, never from the request.
✓from_claim fills tenant_id from the verified token. A user who sends one anyway gets 400; a token without the claim gets 403.
✓QUERYAPIGATE_JWT_ROLE_CLAIM maps a claim such as plan to a role, so each plan gets its own queries, rate limit and quota, counted per user.
✓cache_ttl per query, keyed on each caller's own values, so tenants never share a cached response. ClickHouse or DuckDB underneath for dashboards that load fast.
✓CSV or XLSX for the download button, and every user's runs recorded as jwt:<user>.
# the saved query: the tenant is never a request parameter
curl -X POST https://data.example.com/api/v1/queries \
-H 'X-API-Key: <admin>' -H 'Content-Type: application/json' -d '{
"name": "revenue_by_month", "description": "Monthly revenue",
"connection_name": "analytics", "cache_ttl": 60,
"sql": "SELECT month, revenue FROM tenant_revenue WHERE tenant_id = :tenant_id",
"parameters": {"tenant_id": {"type": "string", "from_claim": "tenant_id"}}
}'
# users sign in with your token; a claim picks their plan's role
QUERYAPIGATE_JWT_JWKS_URL=https://login.example.com/.well-known/jwks.json
QUERYAPIGATE_JWT_ISSUER=https://login.example.com/
QUERYAPIGATE_JWT_AUDIENCE=analytics-api
QUERYAPIGATE_JWT_ROLE_CLAIM=plan
GET /q/revenue_by_month?format=csv
Authorization: Bearer eyJhbGciOi… # acme's rows, and only acme's
GET /q/revenue_by_month?tenant_id=globex # 400
The example APIs ship with a generated database and one scoped key per scenario, so you can try each of these in a few minutes without connecting anything of your own.