Summary
Database-backed RBAC system. Every gateable action in the app is defined as a permission. Roles bundle permissions. Both roles and direct permissions can be assigned to users. Negations supersede grants.
Scope
- Permissions — stored in DB. Each represents a discrete gateable action. Permissions have a scope:
global (app-wide) or team (resolved within a team context). Registered/synced from a canonical source so the codebase and DB stay aligned.
- Roles — named bundles of permissions stored in DB. Roles have a scope (
global or team) and a tier:
- System global roles — Administrator, Staff, Member, Banned. Apply app-wide. Locked: only Administrators can modify.
- System team roles — e.g., Owner. Apply within a team context. Locked: only Staff or above (global) can modify.
- Team custom roles — defined by team admins within their own team. Manageable by users with appropriate team-level permissions.
- Direct user permissions — users can be granted (or denied) individual permissions independent of any role. Optionally scoped to a specific team.
- Negation — any permission assignment (on a role or directly on a user) can be a grant or a negation. Negations always win, regardless of scope or source.
- Resolution —
$user->can($permission, $team = null):
- Global checks: union of global permissions from all global roles + direct global grants, minus negations.
- Team-scoped checks: above ∪ team-scoped permissions from team role assignments for that team + direct team-scoped grants for that team, minus negations.
- Authorization integration — every authorization check in the app (policies, gates, middleware) resolves through this system. No hardcoded role string checks.
- Replaces the existing UserRole enum — the God/User enum is removed; equivalent access is expressed as roles.
Seeded roles
System global roles:
- Administrator — full access (all global permissions granted).
- Staff — basic staff access for internal team members handling support/operations. Can manage system team roles.
- Member — default global role assigned to every user on sign-up.
- Banned — negates all permissions globally and across all teams.
System team roles:
- Owner — full team-scoped permissions for a team. Assigned automatically to the team creator. Cannot be modified by anyone below Staff (global). Re-assignable only via the explicit ownership transfer flow.
Acceptance criteria
- Permissions and all seeded roles (Administrator, Staff, Member, Banned, Owner) exist after seeding
- New users are auto-assigned the Member global role on sign-up
- Team creators are auto-assigned the Owner team role for their team
- Effective permission set is correctly computed (grants minus negations) for both global and team-scoped checks
- Negations from any source override grants from any source and any scope
- Banned global role blocks all access regardless of any other roles or permissions
- System roles (global or team) cannot be modified except by users with sufficient tier authority
- Team admins can create and manage custom team roles within their own team
- All app authorization flows through this system — no hardcoded role string checks remain
- Admins can manage roles, role-permission assignments, and user-role/permission assignments (UI lives in the Admin Panel)
Summary
Database-backed RBAC system. Every gateable action in the app is defined as a permission. Roles bundle permissions. Both roles and direct permissions can be assigned to users. Negations supersede grants.
Scope
global(app-wide) orteam(resolved within a team context). Registered/synced from a canonical source so the codebase and DB stay aligned.globalorteam) and a tier:$user->can($permission, $team = null):Seeded roles
System global roles:
System team roles:
Acceptance criteria