> ## Documentation Index
> Fetch the complete documentation index at: https://docs.textql.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Module 3 · Write the Guards

> Goal: for the scopes the warehouse can’t enforce (embedded users, sources without per-member auth, narrowing beyond the floor), governed .tql surfaces that fail closed — no scop… (~25 min)

**Goal:** for the scopes the warehouse can't enforce (embedded users, sources without per-member auth, narrowing beyond the floor), governed .tql surfaces that **fail closed** — no scope, no rows.

## 3.1 · The anatomy of a guard

A guard is a governed query surface that requires a trusted scope and refuses to run without it. The scope arrives as a **runtime client attribute** — set by a trusted backend (an embedded app's server, an API integration), never typed by the end user:

```text Prompt theme={null}
Create a governed .tql query surface for [governed_claims/orders]: metrics [paid_amount / order_total] grouped by [region]. Require a trusted scope from the runtime client attributes (e.g. _tql.client_attributes_json.region): if the scope is missing or malformed, fail with an error — never return unscoped rows. Filter rows to the scope. Show me the .tql and render the SQL so I can inspect exactly what runs.
```

<Check>
  **You'll see:** a query surface whose WHERE clause is built from the required scope, with an explicit error path when the scope is absent. That error path is the point: **fail closed** means the failure mode of a broken integration is "no data," never "all data."
</Check>

## 3.2 · The rules that keep guards trustworthy

* **Scope comes from a trusted backend** — a server your team controls sets the attribute. Anything a browser or end user can set is a preference, not a boundary.
* **Never expose the entitlement field as a caller-controlled override** — no "optional region parameter" that quietly widens scope.
* **Centralize shared guards** — one reviewed module that other surfaces import, not a copy-pasted WHERE clause per file (copies drift; one of them will be wrong).
* **Interactive members are scoped differently** — for humans in chat, the boundary is their warehouse identity (Module 1) or a role-scoped connector; guards + client attributes are the pattern for *embedded and API* traffic. Don't build a client-attribute guard and assume it scopes your analysts.

## 3.3 · Wire it to the personas

Point each persona's behavioral context (the [**Context Stack**](/workshops/context-stack/overview) personas) at exactly these governed surfaces — the persona's "allowed data surfaces" list and the enforced surface set should be the same list. Persona says *should*, guard says *can*, and they agree.

### ✅ Checkpoint

* [ ] At least one fail-closed guard exists as governed .tql, and you inspected its rendered SQL
* [ ] Removing the scope attribute produces an error, not unscoped rows — tested
* [ ] Shared guard logic lives in one reviewed module, not copies
* [ ] You can say which traffic guards protect (embedded/API) vs what protects interactive members
