Skip to content

Teams

Introduction

When running nubos:init and selecting Team as the organization type, your application is scaffolded to support team creation and management. Each registered user is assigned a "Personal" team on creation and can create or join additional teams.

Team Creation

Actions

Team creation and deletion logic can be customized by modifying the action class at app/Actions/Teams/CreateTeamAction.php:

php
class CreateTeamAction
{
    public function execute(User $owner, array $data): Team
    {
        return DB::transaction(function () use ($owner, $data): Team {
            $team = Team::query()->create([
                'name' => $data['name'],
                'slug' => $data['slug'] ?? Str::slug($data['name']),
                'owner_id' => $owner->id,
                'personal_team' => $data['personal_team'] ?? false,
            ]);

            $team->users()->attach($owner->id, ['role' => 'owner']);

            $owner->update(['current_team_id' => $team->id]);

            event(new TeamCreated($team));

            return $team;
        });
    }
}

The action wraps everything in a DB::transaction(): it creates the team, attaches the owner as a member with the owner role, updates the user's current_team_id, and dispatches a TeamCreated event.

Inspecting User Teams

User team information is accessible through methods provided by the HasTeams trait, automatically added to App\Models\User during installation:

php
// Access all teams a user belongs to...
$user->teams(): BelongsToMany

// Access all teams owned by the user...
$user->ownedTeams(): HasMany

// Access the user's currently selected team...
$user->currentTeam(): BelongsTo

// Determine if a user belongs to a given team...
$user->belongsToTeam($team): bool

The Current Team

Every user has a "current team" tracked by the current_team_id column on the users table. The SetCurrentTeam middleware updates this value automatically when a user visits a team URL. The RedirectToCurrentTeam middleware uses it to redirect users to their last-used team context.

The Team Object

The Team model provides relationships to inspect its members:

php
// Access the team's owner...
$team->owner(): BelongsTo

// Access all team members (including owner)...
$team->users(): BelongsToMany

The users() relationship includes a role pivot column. Every team member has a role (e.g., owner, member).

Member Management

Adding Members

The AddTeamMemberAction attaches a user to the team's pivot table with an optional role:

php
class AddTeamMemberAction
{
    public function execute(Team $team, User $user, ?string $role = null): void
    {
        $team->users()->attach($user->id, ['role' => $role]);

        event(new TeamMemberAdded($team, $user));
    }
}

Removing Members

The RemoveTeamMemberAction detaches a user:

php
class RemoveTeamMemberAction
{
    public function execute(Team $team, User $user): void
    {
        $team->users()->detach($user->id);

        event(new TeamMemberRemoved($team, $user));
    }
}

Middleware

SetCurrentTeam

The SetCurrentTeam middleware resolves the team from the {team} route parameter (by slug), verifies the authenticated user is a member, stores the team on the request attributes, and updates the user's current_team_id:

php
class SetCurrentTeam
{
    public function handle(Request $request, Closure $next): Response
    {
        $team = $request->route('team');

        if (! $team instanceof Team) {
            $team = Team::query()->where('slug', $team)->first();
        }

        if (! $team) {
            abort(404);
        }

        if (! $request->user()->belongsToTeam($team)) {
            abort(403);
        }

        $request->attributes->set('current_team', $team);

        if ($request->user()->current_team_id !== $team->id) {
            $request->user()->update(['current_team_id' => $team->id]);
        }

        return $next($request);
    }
}

RedirectToCurrentTeam

The RedirectToCurrentTeam middleware redirects unauthenticated paths to the user's last-used team:

php
class RedirectToCurrentTeam
{
    public function handle(Request $request, Closure $next): Response
    {
        $user = $request->user();

        if (! $user) {
            return $next($request);
        }

        $team = $user->currentTeam ?? $user->teams()->first();

        if (! $team) {
            return $next($request);
        }

        $path = $request->path();

        return redirect("/teams/{$team->slug}/{$path}");
    }
}

Routes

All team routes are scoped under /teams/{team} and use the SetCurrentTeam middleware:

php
Route::middleware(['web', 'auth', SetCurrentTeam::class])
    ->prefix('teams/{team}')
    ->group(function (): void {
        Route::get('/dashboard', function () {
            return inertia('Dashboard');
        })->name('dashboard');
    });

Routes are loaded by NubosOrganizationServiceProvider:

php
class NubosOrganizationServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->app['router']->aliasMiddleware('set-current-team', SetCurrentTeam::class);
        $this->app['router']->aliasMiddleware('redirect-to-current-team', RedirectToCurrentTeam::class);

        $this->loadRoutesFrom(base_path('routes/team.php'));
    }
}

Events

The following events are dispatched by team actions:

EventDispatched By
TeamCreatedCreateTeamAction
TeamMemberAddedAddTeamMemberAction
TeamMemberRemovedRemoveTeamMemberAction

All events are located in app/Events/Teams/ and accept the relevant models as readonly constructor properties.

Database Schema

Three migrations are generated:

teams table:

php
Schema::create('teams', function (Blueprint $table): void {
    $table->uuid('id')->primary();
    $table->string('name');
    $table->string('slug')->unique();
    $table->foreignUuid('owner_id')->constrained('users')->cascadeOnDelete();
    $table->boolean('personal_team')->default(false);
    $table->timestamps();
    $table->softDeletes();
});

team_user pivot table:

php
Schema::create('team_user', function (Blueprint $table): void {
    $table->uuid('id')->primary();
    $table->foreignUuid('team_id')->constrained('teams')->cascadeOnDelete();
    $table->foreignUuid('user_id')->constrained()->cascadeOnDelete();
    $table->string('role')->nullable();
    $table->timestamps();
    $table->softDeletes();

    $table->unique(['team_id', 'user_id']);
});

current_team_id on users:

php
Schema::table('users', function (Blueprint $table): void {
    $table->foreignUuid('current_team_id')->nullable()->constrained('teams')->nullOnDelete();
});

Configuration

The generated config/nubos.php for a Team organization:

php
return [
    'organization_type' => 'team',
    'organization_model' => \App\Models\Team::class,
    'has_sub_teams' => false,
];