Docs
Permissions in Tenantx

Permissions in Tenantx

Permissions in Tenantx

Org-scoped vs platform-global

  • Organization permissions live in Spatie’s permissions table with organization_id = that org’s UUID. Checks run with setPermissionsTeamId($organizationId) (usually set in tenant middleware).
  • Platform-global permissions have organization_id = NULL, e.g. subscription.admin. The platform middleware clears team context with setPermissionsTeamId(null) before calling hasPermissionTo('subscription.admin').

Spatie teams = organization

Spatie’s teams feature uses organization_id as the team id for tenant users. Role and permission assignments in model_has_roles / role_has_permissions are tied to that team.

Default kernel roles (Tenantx)

Per-organization Spatie roles seeded with the kernel: organization_admin, admin (workspace-scoped), member, contributor. (Legacy installs may still have staff / teacher until you run migrations and php artisan permissions:sync-default-roles.)

Workspace-scoped role assignments

model_has_roles may set workspace_id (→ workspace_branding) for a row. workspace_id null means the role applies organization-wide (all workspaces, same as historical behavior). When workspace_id is set, that role row counts toward hasPermissionTo() only if the request’s permission workspace context matches (resolved from profiles.default_workspace_id, or workspace_id query/body when workspaces_access_all is true). EnsureWorkspaceContext overrides that context when it runs. Assign workspace-local roles via POST /api/permissions/users/assign-role with optional workspace_id; omit it for org-wide assignment. POST /api/permissions/users/remove-role accepts optional workspace_id to remove a workspace-specific row; omit it to remove the org-wide row only.

Example 1 — org permission in a controller

$user = $request->user();
// EnsureOrganizationAccess middleware sets team id to profile.organization_id
if (! $user->hasPermissionTo('organizations.read')) {
    return response()->json(['error' => 'This action is unauthorized'], 403);
}

Example 2 — platform-global permission

setPermissionsTeamId(null);
if (! $user->hasPermissionTo('subscription.admin')) {
    return response()->json(['error' => 'Forbidden'], 403);
}

Example 3 — React

const canRead = useHasPermission("help_center.read");
// Gate UI with canRead; backend must still enforce the same permission

PermissionGroup / PermissionGroupItem

Settings UIs load PermissionGroup records and their PermissionGroupItem rows to show labeled, ordered checkboxes. They map to underlying Spatie permission names (resource.action); the grouping is presentation only—authorization always uses the permission name.