In Another Language: The Runtime Protocol
The application runtime — reading who signed in and querying QueryMesh — exists ready-made in the TypeScript SDK (@nnumbers/zero/app and @nnumbers/zero/react). The published Go SDK is the management API client, not the runtime; an application in another language follows the runtime protocol, the contract between the platform and the application code, described on this page.
The Identity of Who Signed In
On every request that arrives through the edge, the platform puts a compact JWS in X-NNumbers-Identity — header alg: ES256, typ: nn-identity+jwt, kid — with the payload below. With no authenticated person, the header does not come. A value sent by the browser is removed before anything else.
The application always checks:
- the signature, with the broker's public keys (
NNUMBERS_IDENTITY_JWKS; andNNUMBERS_BROKER_URL/v1/jwkswhen an unknownkidarrives) — keep the keys for a few minutes and do not fetch them again on every request; issequal toNNUMBERS_IDENTITY_ISSUER;audequal toNNUMBERS_APP_ID— an identity handed to another application is not valid here;orgequal toNNUMBERS_ORG_ID;nbfandexp, with up to 60 s of clock tolerance — the identity is valid for 5 minutes.
A header whose signature was not checked proves nothing: there are paths that reach the application without going through the edge (the same namespace, the network permissions, the internal entry). Without the header, the request is anonymous; with the header and any check failing, refuse it — never treat it as a person.
| Claim | What it is |
|---|---|
iss, aud, jti | the issuer, the application of this identity and its identifier |
sub | the person's subject in the organization's IAM |
org, org_slug | the organization |
iam | the issuer of the organization's IAM |
sid | the session in the platform — an opaque reference, not the cookie |
name, email, email_verified, preferred_username | when the account has them |
iat, nbf, exp | the validity; exp is 5 minutes after iat |
The Reserved Paths
The paths under /.nnumbers/ are served by the platform on the application's own address — they never reach the application code:
| Method and path | Response |
|---|---|
GET /.nnumbers/auth/login?return_to=<path> | 302 to the organization IAM sign-in |
GET /.nnumbers/auth/callback | 302 back to return_to, with the session created |
POST /.nnumbers/auth/logout | 303 to /, with the application session ended; requires Origin from the address itself |
GET /.nnumbers/auth/session | 203 {"user": {...} | null, "organization": {"id", "slug"}} |
GET /.nnumbers/v1/context | 203 {"app": {"id"}, "organization": {"id", "slug"}, "auth": {"login_path", "logout_path"}} |
return_to only accepts a path of the address itself (starts with /, not with //). The JSON comes with 203, not 200, and Cache-Control: no-store — that is how the edge relays the platform's response, and the browser reads 203 as success.
Signing out is the application's: it ends its session, and the organization's single sign-on continues — signing in again does not ask for a password while it lasts.
The Broker, from Inside the Cluster
NNUMBERS_BROKER_URL is reachable only from inside the cluster. The application uses it for the two operations:
| Method and path | What it does |
|---|---|
GET /v1/jwks | the public keys that sign the identity |
POST /v1/querymesh/query | with the request's X-NNumbers-Identity, runs the query in the organization's QueryMesh on behalf of that person and returns columns and rows |
The query carries the SQL and, optionally, the row cap:
POST {NNUMBERS_BROKER_URL}/v1/querymesh/query
X-NNumbers-Identity: <the request's identity, already checked>
Content-Type: application/json
{"sql": "SELECT name, regionkey FROM tpch.tiny.nation", "max_rows": 1000}
| Field | What it is |
|---|---|
sql | the query (required); never goes to log |
max_rows | the cap on rows brought back, from 1 to 100,000 (default 100,000); above it, the query is cancelled in QueryMesh |
The QueryMesh address is the one the organization administrator configured (zero orgs integrations set querymesh --url https://…); the application does not choose it. Who queries is the platform, with the person's session token, directly — no token exchange —, and the token does not leave the platform. With no person there is no query: no path falls back to an "application" credential.
Success returns the columns, with the Trino type, and the rows, in column order:
{
"columns": [{"name": "name", "type": "varchar"}, {"name": "regionkey", "type": "bigint"}],
"rows": [["BRAZIL", 1]]
}
Refusals come as {"code", "message"}:
| Status | Code | When | What to do |
|---|---|---|---|
400 | VALIDATION_FAILED | no SQL, or max_rows outside 1–100,000 | fix the request and repeat |
401 | USER_SESSION_REQUIRED | no identity, or the session of who signed in ended | take the person to sign-in and repeat |
401 | IDENTITY_ASSERTION_INVALID | the identity does not check out (signature, issuer, validity, organization, application) | use the identity the platform hands over; never treat it as a person |
409 | ORGANIZATION_INTEGRATION_NOT_CONFIGURED | the organization has no QueryMesh configured, or it is off | the organization administrator configures it |
409 | QUERYMESH_NOT_ENABLED | the application has not turned the querymesh capability on | zero services enable querymesh <service> — takes effect immediately |
502 | QUERYMESH_IDENTITY_REJECTED | QueryMesh did not accept the token of who signed in (issuer, audience or scope) | when the realm lacks the scope QueryMesh requires, the platform administration fixes it — signing in again does not help |
403 | QUERYMESH_ACCESS_DENIED | the organization's data policies denied the query to this person | ask the data administrator for access |
422 or 502 | QUERYMESH_QUERY_FAILED | 422: QueryMesh refused the query or the result went past the cap; 502: QueryMesh answered outside the protocol or asked to continue outside the configured address | check the query and repeat; on 502, talk to whoever administers the QueryMesh |
503 | QUERYMESH_UNAVAILABLE | QueryMesh did not answer or is busy, or the platform is at its query limit | repeat in a moment |
503 | APPLICATION_IDENTITY_NOT_READY | the broker has no keys available right now | repeat; if it persists, zero services doctor organization-iam <service> |
What the Application Never Receives
A client secret (it does not exist: the login client is public), a refresh token, the IAM access token — not even for QueryMesh, because who queries is the platform —, and no means of choosing the person.
Next Steps
Was this page helpful?
Report a problem on this pageDo not send passwords, keys, tokens, or customer data.