02Docs
Boundaries
A boundary is a rule your app cannot cross: stated once in your project, next to everything else it governs, and enforced with no admin bypass. Boundaries come in two kinds: data boundaries on what may happen to your data, and change boundaries on who may change the app itself and what they may touch. There are three families in all: supported data rules and invariants, checked by Z3 against every possible request or transaction in their proof model; backend boundaries on policy and code; and UI boundaries, which lock a part of the interface deterministically or guard it with an AI auditor. Together they are what makes it safe to let people and AI build fast on top of things that must not break.
Data boundaries: what may happen to your data
Data boundaries constrain who may read or write each collection and which states the data can ever reach. Supported rule properties and invariants produce formal proof obligations at deploy. Bounded asks the solver about every possible request or transaction in that obligation, not a sampled test set. A failed blocking obligation returns a concrete counterexample so you can fix it before deploy.
They live in policy.json, but the two enforcement layers differ. Collection rules authorize direct SDK/CLI writes and the callers that invoke functions. A hook can bypass collection rules unless it opts into enforceRules: true, and roles are additive grants. Invariants are the unskippable transaction postconditions: SDK, CLI, functions, hooks, schedules, migrations, and live checkpoints must all preserve them. No service key or admin role bypasses an invariant.
| Boundary | What it holds |
|---|---|
| Rules | Who may directly read, create, update, or delete each collection: ownership, roles, membership. Omitted actions deny; denied reads are hidden as empty 200 responses. |
| Spend & rate caps | A rolling window a summed field can never exceed, per user, per agent, or per tenant. Budgets that hold. |
| Conservation | A total that no write can mint or burn. Balances move; value is neither created nor destroyed. |
| Tenant isolation | Every document carries its tenant, and references cannot cross tenants. |
| Hard limits | Ceilings and floors on fields. The anti-cheat and sanity layer. |
See Verification for how the proofs work and what a counterexample looks like, and Invariants for the full syntax.
Change boundaries: who may change the app, and what they may touch
Every Bounded app can carry a change surface: your team, and even your users if you allow it, ship changes to the running app by prompting. Change boundaries decide who may make a change and what it may reach. They split into two families: the backend (policy, functions, app code) and the UI. The UI family has its own vocabulary, below.
| Boundary | What it holds |
|---|---|
| Backend vs frontend | Whether a change may reach app code only, or policy and functions too. |
| Who may change what | Who may prompt, who may promote a change live, and who may touch policy, backend, or frontend. |
| How changes land | For anyone but the owner, a change arrives as a reviewable variation or proposal, never a silent push to what’s live. |
A prompt-made change either fits the boundaries or it is refused with the crossed boundary named. Every change records who asked, what changed, and which boundaries it passed.
UI boundaries: parts of the interface a prompt can never change
A UI boundary marks part of the running app (routes, a region, a component) that prompt-driven changes may not quietly alter. It is a first-class boundary family, declared in the same policy.json under boundaries.ui, each with an id, a plain-text title and description, and a match on repo paths, routes, or a selector. Every one runs in one of two modes.
| Mode | How it holds |
|---|---|
locked | Deterministic. Any prompt-driven edit that touches a matched path is refused before it can commit: fail-closed, with no model judgment in the loop. This is how the checkout button can never be prompted away. |
guarded | AI-audited. A second model reviews every prompt change against the boundary’s plain-text description and blocks the ones that cross it. Use it for intent a path can’t capture: “the headline must always read Ships in 24 hours.” |
The two modes trade certainty for reach. Locked is as mechanical as a path is: an edit either touches a matched file or it does not, and the runtime decides that before anything commits. Guarded reaches things a path match never could (tone, wording, a promise on the page), but it is a judgment made by a reviewer, not a Z3 proof: best-effort, and honest about it. Reach for locked when the boundary is structural, guarded when it is about meaning.
Open apps: the change surface is policy, too
Boundaries decide what a change may touch. Who is allowed to make one (and who pays for the AI that makes it) is the other half, and it lives in the same policy under openApps. An open app opens its change surface past the owner: teammates, or the public, prompt the running app and watch it get built.
| Control | What it decides |
|---|---|
| Who may prompt | Expression gates in the same rule language as your data rules (e.g. get(/holders/@user.id).balance > 0) decide who sees the widget, who may submit a prompt, and whose prompt actually runs, with per-user rate limits. |
| Who pays | The funding mode for a prompt’s AI: prompter (they bring their own), app-bucket (a shared budget), or owner. |
| How changes land | Whether a prompt lands as a variation or as a proposal the owner reviews before anything ships. |
| Activity feed | A public, read-only feed of the app being built: the prompts and the changes they made, in the open. |
| Fork policy | Whether, and how, others may fork the app into a copy of their own. |
Act-as: an owner can voluntarily step down to a lower role for everyday edits (so a stray prompt can’t fumble a core change) and resume full authority at any time.
How data and change boundaries fit together
Change boundaries govern what may ship. Data boundaries hold no matter what ships. Even a change that slips past review cannot move money outside a cap, mint a balance, or cross a tenant. The data plane enforces its invariant postconditions on every write regardless of how the code above it evolved; applicable rules still authorize the direct caller or explicitly rule-enforced hook.