Semantic Server
The server is the query path. It holds no durable state, writes nothing to the engine, and is the only component that reads table data. It watches compiled models from ConfigMaps and may use an optional shared cache, so replicas are interchangeable and scale horizontally.
No SQL exists until identity, external authorization, and built-in governance have passed.
How a request becomes SQL
Section titled “How a request becomes SQL”Identity
Section titled “Identity”The caller is resolved to a principal, groups, and roles before anything else happens, in one of two modes that differ by security boundary.
| Mode | Identity source | Trust boundary |
|---|---|---|
header |
the X-Semantic-User and X-Semantic-Role headers |
safe only behind a proxy that authenticates the caller and sets the headers |
jwt |
a bearer token validated against the issuer’s JWKS | self-contained, ignores inbound identity headers |
An unverified caller gets 401, a verified caller that policy rejects gets 403, and an external provider that fails or is unavailable gets 503 without falling back to allow.
How the query then authenticates to the engine is covered in Identity and the engine. Claim mapping, accepted algorithms, and the header-mode install guard are in Configuration and deployment.
Discovery is governed too
Section titled “Discovery is governed too”Listing models, metrics, and dimensions runs through the same authentication and policy as querying.
- A metric or column the caller’s roles cannot use is omitted from the listing, rather than shown and refused later, so a listing never reveals the shape of the warehouse.
- Raw metric expressions are not returned unless you opt in, since the name, description, and synonyms are enough for an agent to ground on.
- Discovery uses the built-in role policy only. External authorization covers query and SQL-plan requests, so a provider is not a way to hide names during discovery.
Resolve and authorize
Section titled “Resolve and authorize”Metric and dimension names are resolved against the compiled model, and the request is authorized, all before any SQL string exists. Two checks apply, and both must pass. When a model names an external provider, that provider is consulted first, after request limits and before any cache lookup. The built-in role policy then runs as the statement is compiled.
Built-in governance
Section titled “Built-in governance”The role policy is the gate that always applies. It checks three things:
- the requested metrics against the role’s allowlist,
- the requested dimensions and filters against the role’s denied fields,
- every field inside every requested metric expression.
The last check is what stops a caller reading a forbidden column through an aggregate. Because it runs while the statement is built, an unauthorized request fails to compile. There is no filtered result set and no post-processing step that could be skipped.
Roles do not pool their permissions. One role must permit the whole request on its own, every metric and every field together. This differs from additive Kubernetes RBAC. If grants combined across roles, one role’s denied fields and row filters would be cancelled by another role that lacks them, and a caller holding both would see more than either role allows.
External authorization
Section titled “External authorization”A model can also name an external provider, OPA or Ranger. It adds a second gate and never replaces the built-in policy. Because the provider is consulted before any cache lookup, a cached plan or result cannot bypass its decision, and an unavailable or unclear provider fails closed rather than falling back to allow. The provider evaluates the request and identity, and never receives SQL or raw metric expressions.
| Provider result | Outcome |
|---|---|
| Allow, with no obligations | The request proceeds |
| Deny | 403 |
| Timeout, unreachable, malformed, or any obligation such as a mask or row filter | 503, never a fallback to allow |
Configuring a provider, and the exact input it receives, is covered in Authorization. The decision also becomes part of the cache key, described in Configuration and deployment.
The planner turns the authorized request into one SQL statement in three conceptual moves.
- Join planning. It picks the smallest join tree that connects the datasets the request needs, so a query touching only a dimension table does not detour through the fact table.
- Fan-out safety. If a ratio metric’s denominator aggregates a dimension across a join, summing it directly would multiply it by the number of fact rows. The planner splits those into separate aggregations and deduplicates on the dimension’s primary key.
- Combining split aggregations. The planner stacks the separate aggregations with
UNION ALLand groups the stacked rows by the requested dimensions. The engine computes each aggregation once. - Missing groups. Before metric filters apply, the result has every group that any aggregation in the request has. A split ratio cannot remove a group that another metric returns. When one side of a ratio has no rows for a group, the ratio is NULL for that group.
- Zero denominators. A ratio whose denominator is zero is NULL, because the denominator
is always wrapped in
NULLIF(denominator, 0). - Governance in SQL. The role’s row filters are compiled into the statement, conjoined within a role and unioned across the roles that authorized the request. A predicate may interpolate a caller claim to express a rule like “each user sees their own region.” Ranking and the row limit are applied last.
Everything is rendered through the engine’s dialect, so identifier quoting and literal syntax match the target. Row-filter authoring is covered in Authoring a model.
Execute and report
Section titled “Execute and report”The server runs one statement and returns the rows, the exact SQL, the model version, and a request hash. Every statement begins with a comment holding those same three values, so a query in the engine’s log traces back to the request that caused it.
The same request, from the same identity, against the same model version, produces byte identical SQL every time. Identity includes the principal, separate groups and roles, and all configured claims. A row filter can interpolate a claim. Two callers sharing a policy label but with different tenants compile to different SQL and never share a cached plan.
What bounds a request
Section titled “What bounds a request”The server runs in a fixed-size pod, so every part of a request has a limit, and a request over a limit is refused rather than trimmed. Because there is no pagination yet, a truncated result could not be told apart from a complete one. The specific limits, their defaults, and the memory budget that ties concurrency to result size are in Configuration and deployment.
Three surfaces, one compiler
Section titled “Three surfaces, one compiler”Agents connect over MCP and call list_models, list_metrics, list_dimensions, and
query_metric. list_dimensions returns only fields with an Ossie dimension declaration.
The tool descriptions and the model’s own synonyms let an agent ground a
user’s wording onto a certified metric. The agent never writes SQL. How an agent request
becomes SQL shows the complete exchange and explains how the planner
generates SQL without an LLM.
Applications use REST. POST /v1/models/{model}/query runs a request.
POST /v1/models/{model}/sql returns the plan without executing it, which is useful in
review and in tests.
BI tools read the governed views directly from the engine over its native protocol.
All three go through the same compiler, so they cannot disagree about what a metric means.
Caching
Section titled “Caching”An optional Valkey cache stores compiled plans and query results. Caching is an optimisation and never a correctness dependency, so a cache miss or an unreachable cache degrades to recompute and rerun. Cache keys include the full identity and the external authorization decision, so no entry crosses an identity or survives a revoked decision. The exact key composition is in Configuration and deployment.
Access it needs
Section titled “Access it needs”| Target | Access | Why |
|---|---|---|
| Engine model tables | SELECT | Executes the compiled statement |
configmaps |
get, list, watch | An informer keeps compiled models in memory |
| Valkey | Optional | Plan and result caches |
| JWKS endpoint | Read, in jwt mode | Validate bearer tokens |
| OPA or Ranger PDP endpoint | Read, when a model names that provider | Authorize query and SQL-plan requests |
It needs no DDL of any kind, no access to SemanticModel resources, and no catalog or
cloud credentials.
Scaling and deployment
Section titled “Scaling and deployment”The server is stateless, so replicas are interchangeable and you scale horizontally on request rate. Determinism makes the caches effective, so repeated questions can skip the engine entirely. A header-mode deployment must sit behind something that authenticates callers. Service exposure and the chart’s safeguards are in Configuration and deployment.
Access and credentials sets out the full grant matrix for all three components.