Field policy
owner, shared and write_roles decide which records a member reaches and may write
(Owner and shared entities). Field policy decides, record by record, which of a record's fields
that member sees, may change, and must fill in. Air-Flex enforces it on every read and write, whatever path the
request takes — a page, a function, the API or your coding agent — so your code never filters fields itself.
Declaring rules
Rules are [[policy]] tables of the entity's file, db/schema/<entity>.toml:
[[policy]]
field = "internal"
effect = "visible"
when = { ref = "actor.roles", op = "contains", value = "staff" }
[[policy]]
field = "seats"
effect = "writable"
when = { and = [{ ref = "record.status", op = "eq", value = "held" }, { ref = "env.now", op = "lt", value = "2027-01-01T00:00:00Z" }] }
[[policy]]
field = "slot"
effect = "required"
when = { ref = "actor.org_path", op = "in", value = ["/crm/companies"] }
| key | what it says |
|---|---|
field |
a field the entity declares |
effect |
visible: the field is shown only where when holds; writable: it may be changed only where when holds; required: it must be filled in wherever when holds |
when |
a condition: a leaf { ref, op, value }, or and / or over a list of conditions |
A leaf's ref reads one of:
record.<field>— the record's own value (on a create, the value submitted; forrequired, the value the write would leave);actor.roles,actor.sub,actor.org_path— the member acting, with the roles they hold at the record's node, not at their home (How authorization works);env.now— the current time, in steps the platform declares, so a rule's answer does not change every instant;surface— where the request came from:api,mcp(your coding agent),import(your repository's own data — see below), or theui.formandui.tableyour own pages declare.
A record.<field> leaf takes the operators a query filter takes on that field's type, and a condition is bounded
in depth and size as a query filter is. aircloud check refuses a rule outside the grammar — an unknown key,
effect, reference, operator or combinator, a field the entity does not declare, an operator its type refuses, a
condition past the bounds — with schema.policy_invalid; there is no expression escape hatch.
What a member sees
- Rules only take away. Several rules on one field all apply: every
visibleandwritablecondition must hold, and anyrequiredcondition that holds requires the field. A node below the root can add rules of its own that tighten yours, never relax them. - A hidden field is omitted from that record in every answer: a get, a list page, a query, an aggregate.
- A hidden field matches nothing. A filter on a field hidden on a record never makes that record match, in either direction; a sort reads it as absent; an aggregate takes no value from it; a full-text or vector search never finds the record through it. Two records that differ only in a hidden value answer alike, so nothing leaks through a query.
- A field hidden for the whole request — by a rule of yours that reads neither the record nor the member's
roles — is refused up front when a query names it (
flex.forbidden). - A write that changes a field that is not writable on that record is refused (
flex.forbidden, naming the field, the effect and the node). Sending the value it already holds is not a change.
import is your repository
Data your repository declares — seed templates applied at a new node, your data migrations, and the test fixtures
the release verification pipeline loads — arrives on the import surface. It is yours: the repository is the one
writer of your schema, so visible and writable rules do not apply to import. A fixture or a migration can
fill a field you guard for your members — a stage only a manager may change, say — without a rule that names
surface. required still applies: a fixture, seed or migration that leaves a required field empty is refused
(flex.validation). Everything that is not field policy still applies too: write_roles, owner and shared,
the tenant and node a record belongs to, and denies.
Only the platform puts a request on import. Nothing a page, a function, your coding agent or an API call sends —
a header, a query parameter, a body value — makes a request import, so a rule that guards a field from your
members guards it on every one of those paths.
ui.form and ui.table are presentation
Your own page declares the ui.* surface it renders, so a rule whose only difference between showing and hiding a
field is ui.form against ui.table hides nothing from someone who calls the API and declares the other one. Use
them to tidy a screen; protect a confidential field with actor.* or record.*.
Asking why
aircloud authz explain … --entity <entity> --field <field> names the rule that decided whether a member may act
on one field at a node (How authorization works).