AuthContext.roles and the active tenant in AuthContext.tenant_id. Policies reference them through auth.hasRole(...) and auth.tenantId. More complex authorization stays in policy expressions or server functions.
This page covers the data model, the policy syntax, and the patterns for typical multi-tenant apps.
Roles
AuthContext.roles is a string array. Organization roles must be built-ins or declared in the app manifest:
/api/auth/select-org, the runtime loads the matching OrgMember.role into the session so policy auth.hasRole(...) / auth.hasAnyRole(...) checks see them on every request. (There is no auth.roles array binding in the policy DSL. Roles are reachable only through those two functions.)
Special roles
Organization owners and admins receive only their exact org role in
roles. The framework org-management routes authorize those built-ins separately. Custom roles have no inheritance: a reviewer does not satisfy member or admin.
An admin context with an active org is a special case. Its writes still bypass every policy. Its reads do not. Pylon scopes an admin read to the active org, and the read policy runs. So the admin sees only that org’s rows, like a member. An admin context with no active org (an operator, PYLON_ADMIN_TOKEN, or Studio) bypasses reads too.
Policy syntax
Policies live inpylon.manifest.json under policies:
match is the entity name. The other fields are boolean expressions over auth and data:
Supported operators:
==, !=, <, <=, >, >=, &&, ||, !, parentheses, plus numeric literals and the now binding. Ordering compares numbers numerically and ISO-8601 strings chronologically (deny-safe otherwise). There is no arithmetic and no in/ends_with/starts_with. Role checks use the auth.hasRole(...) / auth.hasAnyRole(...) functions; cross-entity membership uses exists(Entity where field == <expr> [and ...]). String matching belongs in a function.
Common patterns
Row-scoped to author
Only the author can read or write their row:Row-scoped to tenant
Multi-tenant apps where rows belong to an org and members of that org can access them:tenant_id flows in through the session. See Sessions → Multi-tenant.
Role-gated mutation
Read is open to all members; write requires elevation:Public read, owner write
Common for blog posts / public profiles:Soft “no one but admin”
Most internal/system tables:"delete": "false" blocks delete entirely. Admins can read and write but never remove.
Where policies run
Policies enforce on every entity-level operation: CRUD via/api/entities/*, sync push, and query reads. They do not run inside server functions. Once you are in a mutation or action handler, you have direct DB access via ctx.db. Use functions when a policy is too complex to express as an expression.
Reading roles in functions
Inside amutation / query / action, ctx.auth exposes:
ctx.auth carries no hasRole helper and no email. roles is useful for
branching and rendering, but authorization should use the ctx.requireMember
helper. It looks up the membership row and fails closed:
requireMember reads.
Granting roles
Role assignments are strings on the membership table. Prefer the built-inPUT /api/auth/orgs/:orgId/members/:userId endpoint, which validates them against
built-ins plus auth.orgRoles. A custom mutation should apply the same allowlist:
Extending beyond roles
For finer-grained access, build a permission table and check it in policies withexists(Permission where todoId == existing.id and userId == auth.userId). Keep complex rules in a server function when they no longer read clearly as policy expressions.
Admin token
PYLON_ADMIN_TOKEN is set in the environment. A request that passes it as Authorization: Bearer <token> resolves to AuthContext::admin(): isAdmin: true, and every hasRole(...) returns true. This context has no active org, so it bypasses every policy, including reads.
This is for:
- Migrations / backfills (
pylon migrate apply) - Studio (the inspector)
- CLI commands (
pylon export,pylon backup) - Server-to-server calls between trusted services
Testing policies
Pylon runs policies in the same evaluator your tests can use:Recipes
- Personal apps — single-user, no roles, no policies. Just
auth.userId != null. - SaaS — tenant_id everywhere, roles on
OrgMember, policies matchdata.orgId == auth.tenantId. - Forum / community —
auth.hasRole('moderator')for soft-delete and ban actions. - Marketplaces — separate
BuyerandSellerroles, policies check both. - Internal tools — single
adminrole, broad gates, audit log via theaudit_logplugin.