Roles and field access
Decide what each signed-in role reads, writes and runs, which columns it sees, and what a signed-out visitor reads, enforced by the server.
technician: { label: 'Technician', // … write: ['maintenance_request', 'request_part', 'part_receipt'], create: ['request_part', 'part_receipt'], commands: ['completeRequest', ...repairs], fields: { resident: { read: ['id', 'name', 'email', 'phone', 'language'] },See it in the sample (examples/apps/lintel/app.ts:66).
What you’ll see
Section titled “What you’ll see”- A technician reads the buildings, the repairs and the storeroom, a resident’s name and how to reach them but nothing else of the resident, edits repairs and parts used, and runs only
completeRequestand the repair messages; a leasing agent edits the rent roll instead. - A column outside a role’s
fieldsmissing from its lists, records, exports and search, as if the table had no such column. - A button, a New form or a menu entry only for the roles that may use it; the server refuses the same request from anyone else.
When to use it, and when not
Section titled “When to use it, and when not”- Use role grants when people differ in what they may read or change; every rule here is checked on the server, whatever the browser sends.
- Use a row policy or a tenant App when people differ in which rows they see, not which tables or columns.
- Use
hidden(secrets) for a column no one may read, signed in or not; a role’sfieldsonly narrows what signed-in roles see.
Who reads what by default
Section titled “Who reads what by default”A resource the App declares reads; access: 'write' is the only other value,
and the only way a resource is written. A table the App does not declare is
not served at all. Writing also needs the connection’s "writable": true in
tablewalk.json, which the adapter enforces whatever the App says.
| The App has | A signed-out visitor | A signed-in person |
|---|---|---|
No auth |
Every declared resource, every column but hidden ones, and writes the 'write' ones |
(no sign-in) |
auth, no roles |
Nothing: the sign-in card | Every declared resource; writes the 'write' ones |
auth and roles |
Only what auth.public names |
What their roles grant together, and nothing else |
With roles, access is denied unless granted. A role that leaves out read
reads nothing, a role the App does not declare grants nothing, and an
account’s roles are checked against the whole policy before any of them is
used, so a broken grant fails closed.
Options
Section titled “Options”| Grant | What it does | Default | Status |
|---|---|---|---|
read |
true for every declared resource, or a list |
nothing | Stable |
write |
true for every 'write' resource, or a list, within what the role reads |
nothing | Stable |
create |
Of what it writes, where it makes new rows: false edits only, or a list |
all it writes | Stable |
export |
Tables it may export as a file, beyond those it writes (which it always may): export: ['charge']. A signed-out visitor never may |
what it writes | Stable |
commands |
The commands it may run; grants no Save, New or Delete | none | Stable |
actions |
Fixed actions by id on a resource it does not write | none | Stable |
fields |
Narrows the columns it reads and writes, per resource | every column | Stable |
label |
The role’s name for people: accounts, a suite’s grants, a tenant’s members | its id as words | Stable |
extends |
Another role whose grants it holds too | none | Stable |
operations |
Operator reads: 'readiness', 'bundle', 'jobs', 'deliveries', 'workflows', 'stores', 'analytics' |
none | Stable |
A person may export a table (the whole list as a CSV or JSON file) when any of
their roles writes it or lists it in export, as the accountant does
(export: ['charge', 'payment']); a signed-out visitor never may, and the
server refuses the file, not just the menu, and a file that reaches another
table through a reference (incident show id, service_id.name), a rollup or a
computed column needs the grant for that table too. A list of a table the
person may not export reads at most 2,000 rows at a time, so it is never a file
by another name. ui.export: false refuses every file, over any grant, and
in an App without role grants it is the one switch.
A role cannot widen a resource’s ceiling. All keys:
the generated reference. Every grant is a line of
authority.lock, so a change in what anyone may do is a reviewed diff.
One role on top of another: extends
Section titled “One role on top of another: extends”A role that does everything another does, and more, says so once:
mechanic: { read: true, write: ['work_order', 'work_order_part'], commands: ['startRepair'] },manager: { extends: 'mechanic', write: ['customer'], commands: ['approveQuote'] },Every grant passes down as the union of both; label, claims and service
stay the role’s own. A loop or a role of another realm is refused, and
authority.lock lists all the manager holds, beneath role manager extends mechanic.
Grants pass down, and so does being named. A view’s or page’s roles open to
a role that extends one they name: roles: ['mechanic'] is opened by a manager
too, through any depth of extends, and roles: ['manager'] is never opened by
the mechanic it extends. Only roles on views and pages work this way. The
principal.roles a row policy reads, a home rule’s
roles and a theme’s for.roles match a role by its own name: list manager
beside mechanic there when the extending role should be matched.
Columns: fields
Section titled “Columns: fields”A resource’s own fields: { read, write } is the column ceiling for every
signed-in role; a role’s fields narrows it further. A role may say only
write to narrow what it edits and keep reading the ceiling. Lintel’s
leasing role edits a viewing’s status, day, home, phone and message but
never its version:
fields: { unit: { write: ['market_rent', 'listed', 'available_on', 'headline', 'description'] }, // A viewing moves along its verbs; a command's revision column (`version`) is never edited in place. viewing: { write: ['status', 'preferred_on', 'unit_id', 'phone', 'message'] },},See it in the sample (examples/apps/lintel/app.ts:26).
Grants and commands meet in four rules, and check names every conflict of
them in one run: a role’s create needs write (it limits what a role that
writes may create, never grants a write); fields only narrows what a role
already reads or writes, so a column it names must be one the role reads;
any role that writes a table a command writes must narrow it with
fields: { table: { write: [...] } } (or the table needs a machine), the
[command-and-edit] refusal; and a command target’s version column is never
in a fields write list, because only commands move a revision.
A column a role does not read is left out of metadata and every answer, and a
query, filter or sort that names it is refused. Primary keys stay readable,
and a written column must be readable. fields needs auth and roles;
without sign-in, keep a column from everyone with hidden. A command’s
handler still reads its footprint whole; what reaches the person stays
narrowed.
Views and pages: roles
Section titled “Views and pages: roles”A list, dashboard, room, signed-in form or page that names roles opens only
for those roles: anyone else finds it missing from their menu, ⌘K and search,
and its address opens nothing, while table grants still apply.
queues: listView('Queues', 'ticket', { roles: ['agent', 'lead'], tabs }),Signed-out visitors
Section titled “Signed-out visitors”auth.public lets a visitor who has not signed in read the
resources it names, read-only, as Lintel opens its listings:
public: { read: ['property', 'property_photo', 'unit'], fields: { property: { read: ['id', 'name', 'address', 'city', 'state', 'zip', 'kind', 'year_built', 'description'] }, // …See it in the sample (examples/apps/lintel/app.ts:127).
Each resource is named; read: true is refused, so nothing is public by
accident. A public resource’s record opens for a visitor at its own address
(/jobs/1), on its page or the one the App builds for it, which names nothing
outside auth.public. A visitor writes nothing except through a
public form. It needs auth.roles, and a
tenant App refuses it: a visitor has no tenant.
A visitor who asks for anything else — a list, a record, a dashboard section
over a table auth.public does not name — is asked to sign in: the read is
answered 401 with the sign-in card, the same answer for a private table and
for a name that is no table, so nothing says which tables exist.
Common mistakes
Section titled “Common mistakes”[outside-ceiling]: a role writes a resource withoutaccess: 'write'→ addaccess: 'write'to the resource; a role grants only within it.[role-cannot-read]: a list, page or record name uses a column some roles cannot read → add the column to theirfields.<table>.read, or show another.[needs-auth]:fieldsin an App without sign-in → usehidden, or addauthwithroles.[write-unreadable-parent]: a role writes a reference to a table it does not read → grant it that table, or leave the column out of its writes.
Related
Section titled “Related”- Sign-in: providers, accounts and the first administrator.
- Row access: which rows each person sees.
- Secrets and hidden columns: what holds without sign-in, and what hides nothing.
- Resources and fields: declared resources,
accessanddelete.