# TenantPilot Architecture Guidelines

Status: 2026-05-15
Applies to: Laravel 12.52, PHP 8.4, Filament 5.2+, Livewire 4, PostgreSQL 16.

## Target Architecture

TenantPilot should remain a Laravel monolith with explicit bounded modules, not a speculative framework. The architecture target is:

- Filament owns admin UI composition only.
- Domain/application services own Intune, backup, restore, audit, evidence, and permission behavior.
- Jobs own long-running or remote Graph work.
- Policies and gates own authorization.
- Models own persistence relationships, casts, scopes, and small invariants only.
- Migrations own data integrity through foreign keys, unique constraints, partial indexes, and JSONB where queryable.

This aligns with the constitution: heavy architecture is allowed for tenant isolation, RBAC, auditability, immutable history, queue correctness, credential safety, and compliance evidence; speculative generic layers are not.

## Current Architecture Signals

Strong patterns already present:

- `GraphClientInterface` is the required external Graph seam.
- `UiEnforcement` and `WorkspaceUiEnforcement` centralize UI authorization behavior.
- `OperationRun` provides observable queued operations.
- `ProviderCredential` uses encrypted casts for credential payloads.
- Workspace/tenant isolation migrations add non-null workspace ownership and composite constraints.
- Pest lanes and architecture/governance tests already exist.

High-risk drift:

- Large Filament classes concentrate UI, authorization, table configuration, modal logic, dispatching, notifications, and domain workflow glue in one place.
- Some resources use static `can*()` methods instead of dedicated policies, making authorization harder to audit globally.
- Historic JSON columns remain mixed with newer JSONB design.

## Rules

- Business logic must not live directly in Filament table/header actions except trivial UI orchestration.
- Every action that creates, mutates, deletes, restores, retries, syncs, dispatches, or exports must call a service/action class or queued job.
- Every new resource-backed model needs a policy, or a documented exception in the feature spec.
- Every tenant-owned query must scope by workspace and managed environment before rendering or mutation.
- Graph calls must never happen during UI render. They must happen in services/jobs through `GraphClientInterface`.
- New abstractions require the constitution proportionality check unless they are security, audit, queue, or isolation-critical.
- Do not add generic provider frameworks until at least two real providers require the variation.
- Prefer extracted builders only when they reduce real review burden. Do not extract one-off schema fragments into a new layer just for style.

## Refactoring Backlog

| Target | Problem | Recommendation | Priority | Effort | Risk if ignored |
|---|---|---|---|---:|---|
| `ManagedEnvironmentOnboardingWizard` | 5,748 LOC workflow page | Split into step schema builders, onboarding draft mutation service, and page-only orchestration. | P1 | L | High regression risk in onboarding and RBAC. |
| `ManagedEnvironmentResource` | 3,785 LOC resource | Extract table columns/filters/actions and tenant-scoped domain actions. | P1 | L | Difficult safe review of destructive environment actions. |
| `RestoreRunResource` | 2,779 LOC resource | Extract restore action builders and write-gate composition. | P1 | M | Restore safety logic becomes hard to audit. |
| `FindingResource` | 2,503 LOC resource | Extract bulk exception/assignment workflows. | P2 | M | Slower feature work and fragile tests. |
| `BackupScheduleResource` | repeated run/retry/bulk closures | Extract `StartBackupScheduleRunAction` service. | P1 | M | Duplicate authorization/audit behavior can drift. |

## Preferred Code Patterns

### Thin Filament Resource

```php
use App\Filament\Resources\BackupScheduleResource\Actions\BackupScheduleActions;
use App\Filament\Resources\BackupScheduleResource\Schemas\BackupScheduleForm;
use App\Filament\Resources\BackupScheduleResource\Tables\BackupScheduleTable;
use App\Models\BackupSchedule;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Tables\Table;

final class BackupScheduleResource extends Resource
{
    protected static ?string $model = BackupSchedule::class;

    protected static bool $isGloballySearchable = false;

    public static function form(Schema $schema): Schema
    {
        return BackupScheduleForm::configure($schema);
    }

    public static function table(Table $table): Table
    {
        return BackupScheduleTable::configure($table);
    }

    public static function makeRunNowAction(): Action
    {
        return BackupScheduleActions::runNow();
    }
}
```

### Service Action for Business Logic

```php
namespace App\Actions\BackupSchedules;

use App\Jobs\RunBackupScheduleJob;
use App\Models\BackupSchedule;
use App\Models\User;
use App\Services\OperationRunService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;

final class StartBackupScheduleRun
{
    public function __construct(
        private readonly OperationRunService $operationRuns,
    ) {}

    public function handle(User $actor, BackupSchedule $schedule): int
    {
        Gate::forUser($actor)->authorize('run', $schedule);

        return DB::transaction(function () use ($schedule, $actor): int {
            $run = $this->operationRuns->startBackupScheduleRun($schedule, $actor);

            RunBackupScheduleJob::dispatch($schedule->getKey(), $run->getKey())
                ->onQueue('graph');

            return (int) $run->getKey();
        });
    }
}
```

### Idempotent Job Skeleton

```php
use App\Models\OperationRun;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\DB;

final class SyncManagedEnvironmentPoliciesJob implements ShouldQueue
{
    use Queueable;

    public int $tries = 3;

    public int $timeout = 300;

    public function __construct(
        private readonly int $operationRunId,
    ) {}

    public function handle(): void
    {
        $run = DB::transaction(function (): OperationRun {
            $run = OperationRun::query()->lockForUpdate()->findOrFail($this->operationRunId);

            if ($run->isTerminal()) {
                return $run;
            }

            $run->markRunning();

            return $run;
        });

        if ($run->isTerminal()) {
            return;
        }

        // Graph work happens here through GraphClientInterface-backed services.
    }
}
```

## Acceptance Standard for New Features

- Spec/plan/tasks exist when code changes runtime behavior.
- Resource/page logic remains UI-focused.
- Mutations have policy authorization, transaction boundaries where needed, audit logging, and tests.
- Remote work is queued and observable.
- Tenant/workspace isolation is proven by tests.
- PostgreSQL-specific behavior is covered in the PostgreSQL lane.
