Skip to content

Roles & Permissions

The StarterKit includes a database-backed authorization system built on Laravel's native Gate and Policy infrastructure. No external packages (like Spatie) are used.

Overview

Roles and permissions are stored in the database. Roles are assigned to users (or other models) with an optional scope, binding the role to a specific organization, workspace, or team. Permissions are attached to roles. Authorization checks flow through Gate::before, which evaluates platform roles, org-level escalation, and scoped permissions.

Two layers of roles exist:

LayerPurposeExamples
PlatformNubos backend administrationnubos:super-admin, nubos:support, nubos:billing
OrganizationApp-level access within an org contextowner, admin, member, team:lead, team:member

Platform roles are always present. Organization roles depend on the structure chosen during nubos:init.

Database Schema

Four tables power the system:

roles -- Role definitions with scope.

ColumnTypeDescription
iduuidPrimary key
namestringRole name, unique per scope
scopestringplatform, tenant, workspace, team, etc.
is_systembooleanSystem roles cannot be deleted

permissions -- Permission definitions.

ColumnTypeDescription
iduuidPrimary key
namestringPermission key, e.g. members.invite
groupstring (nullable)Logical grouping for UI, e.g. members
scopestringSame scope system as roles
is_systembooleanSystem permissions cannot be deleted

role_permission -- Which permissions belong to which role.

role_assignments -- Polymorphic pivot that assigns roles to any model with an optional scope.

ColumnTypeDescription
iduuidPrimary key
role_iduuidFK to roles
model_typestringPolymorphic type (e.g. App\Models\User)
model_iduuidPolymorphic ID
scope_typestring (nullable)Scope model type (e.g. App\Models\Tenant)
scope_iduuid (nullable)Scope model ID

Models

Role

php
use App\Models\Role;

$role = Role::query()->where('name', 'admin')->where('scope', 'tenant')->first();

// Access permissions
$role->permissions; // Collection<Permission>

Permission

php
use App\Models\Permission;

$permission = Permission::query()->where('name', 'members.invite')->first();

// Access roles that have this permission
$permission->roles; // Collection<Role>

RoleAssignment

A MorphPivot model with HasUuids that generates UUIDs for the role_assignments table. Used internally by the HasRoles trait.

The HasRoles Trait

The HasRoles trait is applied to the User model by default. It can be applied to any model (e.g. Team) that needs role assignments.

Assigning and Removing Roles

php
// Platform role (no scope)
$user->assignRole($superAdminRole);

// Scoped role (bound to a specific organization)
$user->assignRole($adminRole, $tenant);

// Remove
$user->removeRole($adminRole, $tenant);

Checking Roles and Permissions

php
// Check role
$user->hasRole('owner', $tenant); // true/false

// Check permission (resolves through assigned roles)
$user->hasPermission('members.invite', $tenant); // true/false

// Get all roles for a scope
$user->rolesFor($tenant); // Collection<Role>

// Get all permissions for a scope (deduplicated)
$user->permissionsFor($tenant); // Collection<Permission>

Authorization Flow

Authorization is handled in AppServiceProvider via Gate::before:

Request
  -> Middleware sets current org context
  -> Controller calls $this->authorize('ability', $model)
  -> Gate::before fires:
       1. nubos:super-admin? -> grant all
       2. owner/admin in any active scope? -> grant all
       3. User has permission in any active scope? -> grant
       4. null -> fall through to Policies

The ScopeResolver determines which scopes are currently active. It is generated by nubos:init based on your organization structure:

Init ChoiceActive Scopes
No init[] (only platform roles work)
Team[current_team]
Workspace[current_workspace]
Workspace + Teams[current_workspace, current_team]
Tenant[current_tenant]
Tenant + Teams[current_tenant, current_team]
Tenant + Workspaces[current_tenant, current_workspace]
Tenant + Workspaces + Teams[current_tenant, current_workspace, current_team]

Permissions are checked across all active scopes. A user with members.invite on the tenant level can also invite in any workspace or team within that tenant.

Platform Roles

Seeded by RolesAndPermissionsSeeder (always present, runs before nubos:init seeders):

RolePermissions
nubos:super-adminAll (via Gate::before, no explicit permissions needed)
nubos:supporttenants.view, users.view, users.impersonate
nubos:billingbilling.view, billing.update, subscriptions.manage

Organization Roles

Seeded by OrgRolesAndPermissionsSeeder (generated by nubos:init):

Base Roles (always generated)

RoleScopeKey Permissions
ownerorg typeAll org permissions (via Gate::before escalation)
adminorg typeorg.settings.view, members.*, teams.* (if sub-teams)
memberorg typemembers.view, teams.view (if sub-teams)

Team Roles (only with sub-teams)

RoleScopeKey Permissions
team:leadteamteam.settings.*, team.members.*
team:memberteamteam.members.view

Org-level owner and admin roles automatically have access to all team-level operations via the escalation in Gate::before.

Policies

Policies live in domain folders under App\Policies:

  • App\Policies\Authorization\RolePolicy -- Protects role management. System roles cannot be updated or deleted.
  • App\Policies\Authorization\PermissionPolicy -- Permissions are read-only in the Starter Kit. Create, update, and delete always return false.

Both are registered on their models via the #[UsePolicy] attribute.

Adding Your Own Permissions

To add permissions for your application's domain, create them in a seeder:

php
use App\Models\Permission;
use App\Models\Role;

// Create the permission
Permission::query()->firstOrCreate(
    ['name' => 'projects.create', 'scope' => 'tenant'],
    ['group' => 'projects', 'is_system' => false],
);

// Assign to existing roles
$adminRole = Role::query()->where('name', 'admin')->where('scope', 'tenant')->first();
$adminRole->permissions()->syncWithoutDetaching(
    Permission::query()->where('name', 'projects.create')->pluck('id'),
);

Then check in your controllers:

php
class ProjectController extends Controller
{
    public function store(StoreProjectRequest $request): RedirectResponse
    {
        $this->authorize('projects.create');

        // ...
    }
}

The Gate::before in AppServiceProvider will resolve the permission against the user's roles in the current scope.

Seeding

Two seeders handle roles and permissions:

SeederLocationContent
RolesAndPermissionsSeederdatabase/seeders/ (always present)Platform roles and permissions
OrgRolesAndPermissionsSeederdatabase/seeders/ (generated by nubos:init)Org-specific roles and permissions

Both are idempotent (firstOrCreate, syncWithoutDetaching) and safe to run multiple times.

The DatabaseSeeder assigns the nubos:super-admin role to the default test user. The generated NubosSeeder assigns org roles to the demo users.