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 hydrates auth.roles with the matching OrgMember.role so policies see them on every request.
Special roles
| Name | What it does |
|---|---|
admin | Bypasses every policy — auth.hasRole('x') returns true for any x |
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:
| Identifier | Type | Meaning |
|---|---|---|
auth.userId | string | null | Current user id, null if anonymous |
auth.isAdmin | bool | True for admin contexts |
auth.tenantId | string | null | Active org id |
auth.hasRole('x') | fn → bool | Role check (admin returns true for any role) |
auth.hasAnyRole('a','b') | fn → bool | Match any of the roles |
data.<field> | varies | Row column value |
==, !=, <, <=, >, >=, &&, ||, !, parentheses.
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 mirrors AuthContext:
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 (data.id in auth.permittedTodoIds), 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.