Permissions in Tenantx
Org-scoped vs platform-global
- Organization permissions live in Spatie’s
permissionstable withorganization_id= that org’s UUID. Checks run withsetPermissionsTeamId($organizationId)(usually set in tenant middleware). - Platform-global permissions have
organization_id = NULL, e.g.subscription.admin. The platform middleware clears team context withsetPermissionsTeamId(null)before callinghasPermissionTo('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.