Anatomy of a contract
The parts of a contract, walked through with Jaffle Shop's product_performance.
A contract is a JSON document. This page walks through each part using the
product_performance contract from the Jaffle Shop sample dataset. For the
exact field-level schema, see the
Contract schema reference.
The example
{
"name": "product_performance",
"description": "Per-SKU performance: units, revenue, COGS, gross-margin ratio.",
"sources": [
{ "source_type": "table", "name": "raw_items", "alias": "i" },
{ "source_type": "model", "name": "orders_clean", "alias": "o",
"join_type": "inner", "cardinality": "many_to_one",
"on": "i.order_id = o.order_id" },
{ "source_type": "model", "name": "products_clean", "alias": "p",
"join_type": "inner", "cardinality": "many_to_one",
"on": "i.sku = p.sku" },
{ "source_type": "model", "name": "sku_costs", "alias": "sc",
"join_type": "left", "cardinality": "many_to_one",
"on": "i.sku = sc.sku" },
{ "source_type": "model", "name": "stores_enriched", "alias": "s",
"join_type": "left", "cardinality": "many_to_one",
"on": "o.store_id = s.store_id" }
],
"dimensions": {
"sku": { "sql": "i.sku", "type": "string" },
"product_name": { "sql": "p.product_name", "type": "string" },
"category": { "sql": "p.category", "type": "string" },
"store_name": { "sql": "s.store_name", "type": "string" },
"ordered_at": { "sql": "o.ordered_at", "type": "timestamp",
"semantic_type": "time",
"time_grains": ["day","week","month","quarter","year"] }
},
"measures": {
"units_sold": { "sql": "count(*)", "type": "number", "additivity": "additive" },
"revenue_usd": { "sql": "sum(p.price_usd)", "type": "number", "additivity": "additive" },
"total_cogs_usd": { "sql": "sum(coalesce(sc.total_cogs_usd,0))", "type": "number", "additivity": "additive" },
"cogs_ratio": { "behavior": "derived", "type": "number",
"numerator": "total_cogs_usd",
"denominator": "revenue_usd" }
}
}Five sources, five dimensions, four measures. The whole contract is one JSON document. Below, each part.
name
A stable identifier matching ^[a-z_][a-z0-9_]*$: lowercase letters,
digits, underscores, not starting with a digit. Callers query the contract
by this name, so treat it like a public API name and avoid renaming a
published contract.
description
Optional human-readable explanation. The natural-language and MCP surfaces read it when picking a contract, so make it informative: what the contract covers, what to use it for, what not to use it for.
sources
sources lists the tables and
models the contract reads. It's an ordered array.
The first source is the contract's anchor (its fact, in the analytical sense). It declares:
source_type:tableormodel.name: the table or model name.alias: the short alias used insqlexpressions throughout the contract.
In product_performance, the anchor is raw_items (one row per line item),
aliased i. Every measure aggregates this fact.
Every additional source is a join, with three more fields:
join_type:left,inner,right, orfull.cardinality: the relationship from the already-joined data to this source:many_to_one,one_to_one, orone_to_many.on: the join condition.
{ "source_type": "model", "name": "products_clean", "alias": "p",
"join_type": "inner", "cardinality": "many_to_one",
"on": "i.sku = p.sku" }For many_to_one and one_to_one joins, the joined source must have a
primary key declared, and the join's
on clause must reference it. That's how Gaur trusts the cardinality you
declared. one_to_many joins don't require a declared key on the joined
side (no uniqueness claim to verify).
In the example, products_clean has sku as its primary key, and the
on clause uses it. Same for orders_clean.order_id, sku_costs.sku,
and stores_enriched.store_id.
cardinality isn't decoration. Gaur uses it to decide whether a contract
is safe. Declaring many_to_one when the join is actually one_to_many
will make queries inflate; the validator catches the structural error at
authoring time and the inflation error at query time. See
Rules and limitations.
dimensions
dimensions are the attributes callers may group and filter by. Each key
is the dimension's name (pattern ^[a-z_][a-z0-9_]*$).
Each dimension has:
sql: the expression producing the value, like"p.product_name".type:string,number,boolean,timestamp, ordate.description: optional explanation.semantic_type: set totimeto mark this as the time axis.time_grains: for time dimensions, the grains a caller may request:hour,day,week,month,quarter,year.
"ordered_at": {
"sql": "o.ordered_at",
"type": "timestamp",
"semantic_type": "time",
"time_grains": ["day", "week", "month", "quarter", "year"]
}A caller can then ask for ordered_at at any declared grain. Requesting a
grain that isn't in the list, or passing time_grain on a non-time
dimension, is rejected.
measures
measures are the values callers may aggregate: sums, counts, averages,
ratios, rolling windows. Measures have their own page; see
Measures for the three kinds (standard,
rolling, derived) and the rules around additivity.
A caller can also filter on a measure. Gaur compiles measure filters as
HAVING clauses, so { "field": "revenue_usd", "operator": "gt", "value": 1000 }
behaves the way SQL users expect. See the
query schema for details.
filters
filters is an optional list of fixed conditions that always apply,
regardless of what a caller asks. Use them to scope a contract to a
relevant slice of data, like excluding cancelled orders or test stores:
"filters": ["o.order_status = 'completed'"]Filters are part of the contract's definition, so every caller sees the same filtered data.
Row-level security (RLS)
Where filters apply equally to everyone, row-level security scopes
data per caller. RLS limits which rows a particular request can see, based
on context values resolved at query time.
An rls block has two fields:
sql: a boolean condition every row must satisfy. It references parameters with{{ placeholder }}tokens.parameters: a map from each placeholder to a context binding.
"rls": {
"sql": "s.store_id = {{ store_id }}",
"parameters": { "store_id": "request.store_id" }
}A context binding has two parts joined by a dot: a source and a field. The source picks which context to read; the field is the key within it.
- An auth binding like
auth.store_idreadsstore_idfrom the auth context attached to the API key. - A request binding like
request.store_idreadsstore_idfrom the requestcontextfield.
So in the example, {{ store_id }} is filled from the request context's
store_id. The placeholder name and the field name are independent: a
parameter named sku bound to auth.product_sku reads product_sku from
the auth context. Resolved values are bound as SQL parameters, never
interpolated as text.
A row is returned only if it satisfies the sql condition. A caller can't
opt out of RLS or widen its own scope.
The two context sources
Auth context
Read with an auth binding. Values are attached to the API key when it's created, so they're identical for every request that key makes and the caller can't change them.
Request context
Read with a request binding. Values are sent by the calling
application per request, in the request context field, so they vary
call to call.
When to use auth context
Auth context fits scoping that's fixed for whoever holds the key. Picture
a partner integration: you issue one API key per
partner, and each key carries that partner's org_id in its metadata.
Every request that key makes is automatically scoped to that one
organization. The partner can't alter it.
Use auth context when the scope is a property of the credential itself.
When to use request context
Request context fits a customer-facing dashboard, where one application serves many end users through one API key. The application can't have a separate key per user, so it scopes them another way.
The user authenticates with your application
Your app's own login or session system establishes who the user is. This has nothing to do with Gaur.
Your backend derives the scoping value
From the authenticated session, your trusted backend determines the value
that scopes this user's data, such as their store_id or user_id.
Your backend sends it as request context
Your backend calls Gaur and includes that value in the request context.
The contract's RLS uses it to return only that user's rows.
{
"contract_name": "product_performance",
"query": { "measures": ["revenue_usd"] },
"context": { "store_id": "STR-008" }
}Set request context on your trusted backend, never from the end user. If
a user can choose their own store_id, they can read other stores' data.
Derive the value from the authenticated session server-side and treat the
end user as untrusted input. Don't surface these values in the client or
let the client set them.
Builders define the rls block. Integrators supply the request context.
The integrator side is in
Querying contracts.