Skip to content
Air-Cloud docs

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:

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

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).