Teams and Permissions
The team system provides fine-grained access control within workspaces through role-based teams with configurable permissions.
Overview
Workspace
├── Teams (permission groups)
│ ├── Owners (system team)
│ ├── Admins (system team)
│ ├── Members (system team, default)
│ └── Custom teams...
└── Members (users in workspace)
└── assigned to Team (or custom_permissions)Quick Start
Check Permissions
use Core\Tenant\Services\WorkspaceTeamService;
$teamService = app(WorkspaceTeamService::class);
$teamService->forWorkspace($workspace);
// Single permission
if ($teamService->hasPermission($user, 'social.write')) {
// User can create/edit social content
}
// Any of multiple permissions
if ($teamService->hasAnyPermission($user, ['admin', 'owner'])) {
// User is admin or owner
}
// All permissions required
if ($teamService->hasAllPermissions($user, ['social.read', 'social.write'])) {
// User has both permissions
}Via Middleware
// Single permission
Route::middleware('workspace.permission:social.write')
->group(function () {
Route::post('/posts', [PostController::class, 'store']);
});
// Multiple permissions (OR logic)
Route::middleware('workspace.permission:admin,owner')
->group(function () {
Route::get('/settings', [SettingsController::class, 'index']);
});System Teams
Three system teams are created by default:
Owners
[
'slug' => 'owner',
'permissions' => ['*'], // All permissions
'is_system' => true,
]Workspace owners have unrestricted access to all features and settings.
Admins
[
'slug' => 'admin',
'permissions' => [
'workspace.read',
'workspace.manage_settings',
'workspace.manage_members',
'workspace.manage_billing',
// ... all service permissions
],
'is_system' => true,
]Admins can manage workspace settings and members but cannot delete the workspace or transfer ownership.
Members
[
'slug' => 'member',
'permissions' => [
'workspace.read',
'social.read', 'social.write',
'bio.read', 'bio.write',
// ... basic service access
],
'is_system' => true,
'is_default' => true,
]Default team for new members. Can use services but not manage workspace settings.
Permission Structure
Workspace Permissions
| Permission | Description |
|---|---|
workspace.read | View workspace details |
workspace.manage_settings | Edit workspace settings |
workspace.manage_members | Invite/remove members |
workspace.manage_billing | View/manage billing |
Service Permissions
Each service follows the pattern: service.read, service.write, service.delete
| Service | Permissions |
|---|---|
| Social | social.read, social.write, social.delete |
| Bio | bio.read, bio.write, bio.delete |
| Analytics | analytics.read, analytics.write |
| Notify | notify.read, notify.write |
| Trust | trust.read, trust.write |
| API | api.read, api.write |
Wildcard Permission
The * permission grants access to everything. Only used by the Owners team.
WorkspaceTeamService API
Team Management
$teamService = app(WorkspaceTeamService::class);
$teamService->forWorkspace($workspace);
// List teams
$teams = $teamService->getTeams();
// Get specific team
$team = $teamService->getTeam($teamId);
$team = $teamService->getTeamBySlug('content-creators');
// Get default team for new members
$defaultTeam = $teamService->getDefaultTeam();
// Create custom team
$team = $teamService->createTeam([
'name' => 'Content Creators',
'slug' => 'content-creators',
'description' => 'Team for content creation staff',
'permissions' => ['social.read', 'social.write', 'bio.read', 'bio.write'],
'colour' => 'blue',
]);
// Update team
$teamService->updateTeam($team, [
'permissions' => [...$team->permissions, 'analytics.read'],
]);
// Delete team (non-system only)
$teamService->deleteTeam($team);Member Management
// Get member record
$member = $teamService->getMember($user);
// List all members
$members = $teamService->getMembers();
// List team members
$teamMembers = $teamService->getTeamMembers($team);
// Assign member to team
$teamService->addMemberToTeam($user, $team);
// Remove from team
$teamService->removeMemberFromTeam($user);
// Set custom permissions (override team)
$teamService->setMemberCustomPermissions($user, [
'social.read',
'social.write',
// No social.delete
]);Permission Checks
// Get effective permissions
$permissions = $teamService->getMemberPermissions($user);
// Returns: ['workspace.read', 'social.read', 'social.write', ...]
// Check single permission
$teamService->hasPermission($user, 'social.write');
// Check any permission (OR)
$teamService->hasAnyPermission($user, ['admin', 'owner']);
// Check all permissions (AND)
$teamService->hasAllPermissions($user, ['social.read', 'social.write']);
// Role checks
$teamService->isOwner($user);
$teamService->isAdmin($user);WorkspaceMember Model
The WorkspaceMember model represents the user-workspace relationship:
$member = WorkspaceMember::where('workspace_id', $workspace->id)
->where('user_id', $user->id)
->first();
// Properties
$member->role; // 'owner', 'admin', 'member'
$member->team_id; // Associated team
$member->custom_permissions; // Override permissions (JSON)
$member->joined_at;
$member->invited_by;
// Relationships
$member->user;
$member->team;
$member->inviter;
// Permission methods
$member->getEffectivePermissions(); // Team + custom permissions
$member->hasPermission('social.write');
$member->hasAnyPermission(['admin', 'owner']);
$member->hasAllPermissions(['social.read', 'social.write']);
// Role checks
$member->isOwner();
$member->isAdmin();Permission Resolution
Effective permissions are resolved in order:
- Role-based: Owner role grants
*, Admin role grants admin permissions - Team permissions: Permissions from assigned team
- Custom permissions: If set, completely override team permissions
public function getEffectivePermissions(): array
{
// 1. Owner has all permissions
if ($this->isOwner()) {
return ['*'];
}
// 2. Custom permissions override team
if (!empty($this->custom_permissions)) {
return $this->custom_permissions;
}
// 3. Team permissions
return $this->team?->permissions ?? [];
}Workspace Invitations
Invite Users
// Via Workspace model
$invitation = $workspace->invite(
email: 'newuser@example.com',
role: 'member',
invitedBy: $currentUser,
expiresInDays: 7
);
// Invitation sent via WorkspaceInvitationNotificationAccept Invitation
// Find and accept
$invitation = WorkspaceInvitation::findPendingByToken($token);
if ($invitation && $invitation->accept($user)) {
// User added to workspace
}
// Or via Workspace static method
Workspace::acceptInvitation($token, $user);Invitation States
$invitation->isPending(); // Not accepted, not expired
$invitation->isExpired(); // Past expires_at
$invitation->isAccepted(); // Has accepted_atCustom Teams
Creating Custom Teams
$team = $teamService->createTeam([
'name' => 'Social Media Managers',
'slug' => 'social-managers',
'description' => 'Team for managing social media accounts',
'permissions' => [
'workspace.read',
'social.read',
'social.write',
'social.delete',
'analytics.read',
],
'colour' => 'purple',
'is_default' => false,
]);Making Team Default
$teamService->updateTeam($team, ['is_default' => true]);
// Other teams automatically have is_default set to falseDeleting Teams
// Only non-system teams can be deleted
// Teams with members cannot be deleted
if ($team->is_system) {
throw new \RuntimeException('Cannot delete system teams');
}
if ($teamService->countTeamMembers($team) > 0) {
throw new \RuntimeException('Remove members first');
}
$teamService->deleteTeam($team);Seeding Default Teams
When creating a new workspace:
$teamService->forWorkspace($workspace);
$teams = $teamService->seedDefaultTeams();
// Or ensure they exist (idempotent)
$teams = $teamService->ensureDefaultTeams();Migrating Existing Members
If migrating from role-based to team-based:
$migrated = $teamService->migrateExistingMembers();
// Assigns members to teams based on their role:
// owner -> Owners team
// admin -> Admins team
// member -> Members teamBest Practices
Use Middleware for Route Protection
Route::middleware(['auth', 'workspace.required', 'workspace.permission:social.write'])
->group(function () {
Route::resource('posts', PostController::class);
});Check Permissions in Controllers
public function store(Request $request)
{
$teamService = app(WorkspaceTeamService::class);
$teamService->forWorkspace($request->attributes->get('workspace_model'));
if (!$teamService->hasPermission($request->user(), 'social.write')) {
abort(403, 'You do not have permission to create posts');
}
// ...
}Use Policies with Teams
class PostPolicy
{
public function create(User $user): bool
{
$teamService = app(WorkspaceTeamService::class);
return $teamService->hasPermission($user, 'social.write');
}
public function delete(User $user, Post $post): bool
{
$teamService = app(WorkspaceTeamService::class);
return $teamService->hasPermission($user, 'social.delete');
}
}Permission Naming Conventions
Follow the pattern: service.action
service.read- View resourcesservice.write- Create/edit resourcesservice.delete- Delete resourcesworkspace.manage_*- Workspace admin actions