AuthContext carries a roles: string[] array and (optionally) a tenant_id. Policies reference both via auth.hasRole(...) and auth.tenantId. There’s no role hierarchy, no permission grids, no @CanRead("Todo") decorators — just expressions over the auth context and the row.
This page covers the data model, the policy syntax, and the patterns for typical multi-tenant apps.
Roles
AuthContext.roles is a string array. You decide the role names. Common choices:
/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’s no auth.roles array binding in the policy DSL — roles are only reachable through those two functions.)
Special roles
That’s it. No other role has built-in meaning.
admin is granted automatically when a request authenticates with PYLON_ADMIN_TOKEN; it’s not something you give to user accounts.
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). 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 via 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/write but never remove.
Where policies run
Policies enforce on every entity-level operation: CRUD via/api/entities/*, sync push, query reads. They do not run inside server functions — once you’re in a mutation or action handler, you have direct DB access via ctx.db. Functions are the right place for “policy too complex to express as an expression” cases.
Reading roles in functions
Inside amutation / query / action, ctx.auth exposes:
ctx.auth carries no roles array, no hasRole, and no email —
those are policy-expression / session concerns. For the admin case, ctx.auth.isAdmin
is enough. To gate a function on org membership or a role, use the first-class
ctx.requireMember helper — it looks up the membership row and fails closed:
requireMember reads.
Granting roles
Roles aren’t first-class data — they’re just strings on whatever join table you use. Typical “promote to admin” mutation:Why no permission grid?
Pylon ships role-based access control, not attribute-based or capability-based. You could build a permission table and check it in policies withexists(Permission where todoId == existing.id and userId == auth.userId), but for 90% of apps “owner can write, member can read, admin bypasses” is enough — and policies stay readable.
If you outgrow it, the policy expression language is intentionally narrow so you can move complex logic into a function without losing security guarantees.
Admin token
PYLON_ADMIN_TOKEN is set in the environment. Requests that pass it as Authorization: Bearer <token> resolve to AuthContext::admin() — isAdmin: true, every hasRole(...) returns true, every policy is bypassed.
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.