Skip to content

Concept Overview

Actions

The StarterKit does not publish controllers to your application. Instead, backend logic is encapsulated in "Action" classes. During the nubos:init process, actions are published to your application's app/Actions directory.

Action classes perform a single operation and follow a consistent pattern: a public execute() method that receives typed parameters, wraps writes in DB::transaction() when multiple database operations are involved, and dispatches a domain event on completion.

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

Each organization type generates three actions: Create{Model}Action, Add{Model}MemberAction, and Remove{Model}MemberAction. You are free to customize these classes.

Views / Pages

The StarterKit uses Inertia.js with Vue 3. Pages are published to your resources/js/Pages directory. Routes return Inertia responses:

php
Route::get('/dashboard', function () {
    return inertia('Dashboard');
})->name('dashboard');

There is no Blade or Livewire. All frontend rendering happens through Vue single-file components with <script setup lang="ts">.

Middleware

The StarterKit generates middleware that resolves the current organization context from the URL. This approach is session-independent, meaning users can work with multiple organizations in separate browser tabs simultaneously.

For Team and Workspace organizations, SetCurrent{Model} middleware resolves the organization from a route parameter, verifies membership, and stores the context on the request:

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

For Tenant organizations, TenantIdentification middleware resolves the tenant from the subdomain instead of a route parameter.

A companion RedirectToCurrent{Model} middleware redirects unauthenticated URL paths to the user's last-used organization.

Traits

Instead of model inheritance, the StarterKit uses traits to add organization capabilities to models.

User model traits are injected automatically by nubos:init:

  • HasTeams / HasWorkspaces -- Adds teams() / workspaces(), ownedTeams(), currentTeam(), and belongsToTeam() relationships and methods to the User model.
  • BelongsToTenant -- Adds tenants(), currentTenant(), and belongsToTenant() to the User model.

Organization model traits are applied to organization models when they run under a tenant:

  • TenantScope -- A global scope that automatically filters all queries by the current tenant's tenant_id and auto-sets tenant_id on new records. Applied to Team/Workspace models when they exist as sub-organizations within a tenant.

Events

Actions dispatch domain events that you can listen to. Every event uses Dispatchable and SerializesModels, and receives the relevant model(s) as readonly promoted constructor properties:

php
class TeamCreated
{
    use Dispatchable;
    use SerializesModels;

    public function __construct(
        public readonly Team $team,
    ) {}
}

The naming convention is {Model}{Action}:

EventDispatched By
TeamCreatedCreateTeamAction
TeamMemberAddedAddTeamMemberAction
TeamMemberRemovedRemoveTeamMemberAction

The same pattern applies to Workspace and Tenant organizations.

Configuration

The nubos:init command generates a config/nubos.php file that records your choices. This file is read-only after generation -- it serves as a record of which organization type was scaffolded:

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

The configuration varies by organization type and selected options. For tenant organizations it includes additional keys like database_strategy and sub_organization.