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.

Authorization

The StarterKit ships with a database-backed roles and permissions system. Roles are assigned to users with an optional scope (the organization, workspace, or team). Permissions are attached to roles. All authorization checks use Laravel's native Gate and Policy infrastructure.

A Gate::before callback in AppServiceProvider handles three escalation levels:

  1. Platform super-admin (nubos:super-admin) -- bypasses all checks.
  2. Org owner/admin -- full access within their organization's scope.
  3. Permission match -- checks if the user has the requested permission in any active scope.

The active scopes are resolved by a ScopeResolver service. The default implementation returns an empty array. During nubos:init, it is replaced with a variant-specific implementation that reads the current organization context from the request (e.g. current_tenant, current_workspace, current_team).

php
// In a controller -- authorization always happens here, never in FormRequests
$this->authorize('members.invite');

Policies live in domain folders (App\Policies\Authorization\) and are registered on models via #[UsePolicy] attributes. See the Roles & Permissions documentation for the full reference.

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,
];

For the None organization type, the config is minimal:

php
return [
    'organization_type' => 'none',
    '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.