Access control
Policies grant actions, roles carry policies, and every request is checked against them. Deny by default, everywhere.
Access control is one mechanism used everywhere. A principal (a user, an agent, a scheduled job) holds roles; roles carry policies; a policy grants actions. Every request, every handler SDK call and every agent tool call is checked against the caller's policies, live. Nothing is allowed unless a policy allows it.
invoices, vendors, payments). Your app has only the tables you create, so put your own table and column names in their place. About the example appThis page is the model and the rules. Assigning roles covers giving people and agents their roles, and Protecting data covers redaction, locks and protected tables. The governance model explains why it is built this way.
Actions
An action is a key, resource:action:
invoices:read_list
invoices:create
orders:refund
org_invitations:orgInvitesCreate- Your tables each get five, as soon as the table exists:
create,read,read_list,updateanddelete. Nobody holds them until a policy grants them. - Custom actions you add to a table carry their own key, which is what lets you grant
orders:refundwithout grantingorders:update. See Custom actions. - Authoric's own features (sign-in, invitations, storage, agents, scheduled jobs) have keys of their own. The console's policy editor groups them into named capabilities, such as "Org invites" or "Two-factor authentication", so you switch on a feature rather than ticking eight keys.
When a request is refused, the 403 message names the action: Access denied: 'invoices:create' — no policy bound to you allows it. Grant that key, or a capability containing it.
Policies
A policy is a named set of rules, keyed by action:
{
"version": "1.0",
"defaultEffect": "deny",
"actions": {
"invoices:read_list": [{ "effect": "allow" }],
"invoices:read": [{ "effect": "allow" }],
"invoices:update": [{ "effect": "allow", "when": ["isOwner"] }],
"invoices:delete": [{ "effect": "deny" }]
}
}In the console you build policies under App access › Policies: capability switches for Authoric's features, five checkboxes for each of your tables, a switch per handler or tool, and an advanced editor for individual rules.


How rules resolve
When several rules match one action, in order:
- An explicit deny you wrote on that exact action wins, whatever its priority. "Grant the
whole capability, except delete" works the way it reads. (A
*wildcard deny does not get this power, so one broad rule cannot lock you out of fixing it.) - A rule naming the exact action beats a
*wildcard. - A more specific binding beats a broader one: one person, then a role, then the organization, then everyone.
- Lower
prioritynumbers win, then deny beats allow on a tie.
Conditions
A rule's when list must all pass for the rule to apply:
| Condition | Passes when |
|---|---|
isSelf, notSelf | The record is (or is not) the caller's own account |
isOwner | The record's owner_id, user_id or created_by is the caller |
hasRole:<key> | The caller holds that role |
argMax:<field>=<n>, argMin:<field>=<n> | A tool call's numeric argument is at most / at least n |
argEquals:<field>=<v>, argIn:<field>=<a>|<b>, argNotIn:…, argPresent:<field> | A tool call's argument matches |
isSelf and isOwner test the one record a request addresses, so they apply to read, update
and delete of a single record, never to lists or to actions that address no record. Organization
isolation needs no condition at all: inside the app it is already applied to every query (see
Organizations).
The argument conditions are how "may refund" becomes "may refund up to $50":
{
"actions": {
"mcp_stripe:create_refund": [{ "effect": "allow", "when": ["argMax:amount_cents=5000"] }]
}
}They read the arguments of an agent tool call, an MCP tool call, or the same call made from a
handler. They fail closed: a missing or unreadable argument does not satisfy the condition, so
omitting amount_cents cannot skip the ceiling.
Roles
Policies are bound to roles, and roles to principals. A principal never holds a policy directly (with one exception, below), so "what can this person do" is answered in one place, and removing a policy from a role removes it from everyone holding the role at once.
Users, agents and scheduled jobs are all given roles the same way. An agent's tools are derived from the policies on its roles, so granting an agent an ability is the same act as granting a person one.


What a new app grants out of the box
Deny is the default everywhere. A newly provisioned app grants only this:
| Who | Holds | Can |
|---|---|---|
| Anyone, signed in or not | The seeded auth grants | Sign up, sign in by every method, answer a 2FA challenge, refresh, sign out, read /auth/methods |
| Every member of an organization | Org Member Baseline (bound to each person, not a role) | Switch and leave organizations, handle invitations addressed to them, manage their own sign-in methods, 2FA and sessions |
user role | Org Member Read, Org Member Self-Edit, App Product Purchase | Read users in their organization, edit their own account, buy your products |
admin role | Org Admin Management, Org Invite and Accept | Manage the organization's users, invitations and members, rename it, set its 2FA requirement |
superuser role | The above, plus Org Owner | Also create roles in the organization and assign them, within what they hold |
Someone who signs up holds user and superuser in the organization they created. Someone who
joins by invitation holds no role, only the baseline.
Nobody can read your tables until you say so
No seeded role grants anything on the tables you create, or on your handlers, endpoints, agents or
scheduled jobs. Those are your app's structure. Grant your users exactly the table actions they
need by putting them in a policy on the user role (or a role of your own). Allow All exists as
a seeded policy but is bound to nothing; binding it hands over your app's structure as well as its
data.
Seeded policies are wiring: they cannot be edited or unbound (423 POLICY_IS_SYSTEM). Seeded roles
cannot be deleted (ROLE_PROTECTED). Make your own policy and bind it instead.
Errors to branch on
| Status | Code | Meaning |
|---|---|---|
| 403 | (none) | No policy bound to the caller allows the action. The message names it. |
| 403 | GRANT_EXCEEDS_HOLDER | Someone tried to grant a role or policy carrying actions they do not hold. The response names them. |
| 403 | POLICY_ROLE_SCOPE_MISMATCH | An organization's own policy can only be bound to that organization's roles. |
| 403 | STAFF_ONLY | The action belongs to Authoric's own staff. Nothing you can grant reaches it. |
| 423 | POLICY_IS_SYSTEM | A seeded policy cannot be edited or unbound. |
| 423 | TABLE_LOCKED | The table is locked against this operation. See Protecting data. |