RBAC & Access Levels
Three distinct role systems gate the platform. Permissions are checked one way — User.has_permission — with resource.action strings.

Three role systems
| System | Gates | Surfaced as |
|---|---|---|
Coarse User.role enum | broad identity (user / admin / guest) | — |
AdminRole | the admin app (/admin) | “Roles” |
AccessLevel | the user app (/user) | “Access Levels” |
Admin roles and user access levels are separate grant systems; the live check for both is User.has_permission(...).
Permission format
Permissions are resource.action strings — e.g. invoices.manage, cms.manage, settings.manage. Wildcards are supported (* = superadmin; cms.* = all CMS actions). Edit pages distinguish view from manage.
Declaring permissions from a plugin
# on the plugin object
permissions = [
{"key": "myfeature.view", "label": "View my feature"},
{"key": "myfeature.manage", "label": "Manage my feature"},
]
# guard a route
@require_auth
@require_permission("myfeature.manage")
def update(): ...GDPR
Never expose one user's personal data to another. Personal-data entities (users, user details, invoices) are GDPR-blocked from cross-entity search and are never broadcast on the event bus. Owner-scoped features must fail-closed: if an owner can't be resolved, deliver nothing.