# 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.