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:
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:
public function teams(): HasMany
{
return $this->hasMany(Team::class);
}CreateTeamAction (within Workspace)
When teams exist within workspaces, the CreateTeamAction requires a Workspace parameter:
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:
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:
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:
return [
'organization_type' => 'workspace',
'organization_model' => \App\Models\Workspace::class,
'has_sub_teams' => false,
];Workspace with teams:
return [
'organization_type' => 'workspace',
'organization_model' => \App\Models\Workspace::class,
'has_sub_teams' => true,
'sub_team_model' => \App\Models\Team::class,
];