GaurGaur docs
Contracts

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: table or model.
  • name: the table or model name.
  • alias: the short alias used in sql expressions 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, or full.
  • cardinality: the relationship from the already-joined data to this source: many_to_one, one_to_one, or one_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, or date.
  • description: optional explanation.
  • semantic_type: set to time to 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_id reads store_id from the auth context attached to the API key.
  • A request binding like request.store_id reads store_id from the request context field.

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.

Next

On this page