Skip to content

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

php
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

php
// 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

php
[
    'slug' => 'owner',
    'permissions' => ['*'],  // All permissions
    'is_system' => true,
]

Workspace owners have unrestricted access to all features and settings.

Admins

php
[
    '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

php
[
    '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

PermissionDescription
workspace.readView workspace details
workspace.manage_settingsEdit workspace settings
workspace.manage_membersInvite/remove members
workspace.manage_billingView/manage billing

Service Permissions

Each service follows the pattern: service.read, service.write, service.delete

ServicePermissions
Socialsocial.read, social.write, social.delete
Biobio.read, bio.write, bio.delete
Analyticsanalytics.read, analytics.write
Notifynotify.read, notify.write
Trusttrust.read, trust.write
APIapi.read, api.write

Wildcard Permission

The * permission grants access to everything. Only used by the Owners team.

WorkspaceTeamService API

Team Management

php
$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

php
// 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

php
// 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:

php
$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:

  1. Role-based: Owner role grants *, Admin role grants admin permissions
  2. Team permissions: Permissions from assigned team
  3. Custom permissions: If set, completely override team permissions
php
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

php
// Via Workspace model
$invitation = $workspace->invite(
    email: 'newuser@example.com',
    role: 'member',
    invitedBy: $currentUser,
    expiresInDays: 7
);

// Invitation sent via WorkspaceInvitationNotification

Accept Invitation

php
// 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

php
$invitation->isPending();  // Not accepted, not expired
$invitation->isExpired();  // Past expires_at
$invitation->isAccepted(); // Has accepted_at

Custom Teams

Creating Custom Teams

php
$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

php
$teamService->updateTeam($team, ['is_default' => true]);
// Other teams automatically have is_default set to false

Deleting Teams

php
// 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:

php
$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:

php
$migrated = $teamService->migrateExistingMembers();
// Assigns members to teams based on their role:
// owner -> Owners team
// admin -> Admins team
// member -> Members team

Best Practices

Use Middleware for Route Protection

php
Route::middleware(['auth', 'workspace.required', 'workspace.permission:social.write'])
    ->group(function () {
        Route::resource('posts', PostController::class);
    });

Check Permissions in Controllers

php
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

php
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 resources
  • service.write - Create/edit resources
  • service.delete - Delete resources
  • workspace.manage_* - Workspace admin actions

Released under the EUPL-1.2 License.