Skip to content

Workspace

Introduction

When running nubos:init and selecting Workspace as the organization type, your application is scaffolded with workspace support. The generated code is structurally identical to the Team organization -- the same models, actions, middleware, and events are generated, but with Workspace as the model name instead of Team.

Each user gets a personal workspace on registration and can create or join additional workspaces. A workspace has an owner and members with roles.

Workspace with Teams

When selecting Workspace, the nubos:init command asks:

Enable teams within workspaces?

If you confirm, the scaffolding generates both Workspace and Team models, creating a two-level hierarchy: Workspace > Team. A team always belongs to a workspace. Users can be members of both workspaces and individual teams within them.

Team Model (within Workspace)

The Team model generated for workspace-teams has a workspace_id foreign key:

php
class Team extends Model
{
    use HasFactory;
    use HasUuids;
    use SoftDeletes;

    public $incrementing = false;
    protected $keyType = 'string';
    protected $fillable = [
        'workspace_id',
        'name',
        'slug',
        'owner_id',
    ];

    public function workspace(): BelongsTo
    {
        return $this->belongsTo(Workspace::class);
    }

    public function owner(): BelongsTo
    {
        return $this->belongsTo(User::class, 'owner_id');
    }

    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class)
            ->withPivot('role')
            ->withTimestamps();
    }
}

The Workspace model gets a teams() relationship injected:

php
public function teams(): HasMany
{
    return $this->hasMany(Team::class);
}

CreateTeamAction (within Workspace)

When teams exist within workspaces, the CreateTeamAction requires a Workspace parameter:

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

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

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

            event(new TeamCreated($team));

            return $team;
        });
    }
}

Nested Middleware

The SetCurrentTeam middleware for workspace-teams validates that the team belongs to the current workspace before checking membership:

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);
        }

        $currentWorkspace = $request->attributes->get('current_workspace');

        if (!$currentWorkspace) {
            abort(403, 'Workspace context required before team resolution.');
        }

        if ($team->workspace_id !== $currentWorkspace->id) {
            abort(403, 'Team does not belong to current workspace.');
        }

        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);
    }
}

Nested Routes

Team routes are nested under the workspace prefix. The middleware stack applies SetCurrentWorkspace first, then SetCurrentTeam:

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

Redirect Middleware

When workspace-teams is enabled, the RedirectToCurrentWorkspace middleware is replaced by RedirectToCurrentOrg, which handles redirecting to the correct workspace (or workspace + team) context.

Configuration

Workspace without teams:

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

Workspace with teams:

php
return [
    'organization_type' => 'workspace',
    'organization_model' => \App\Models\Workspace::class,
    'has_sub_teams' => true,
    'sub_team_model' => \App\Models\Team::class,
];