# Implementation Plan: Workspace & Tenant Closure Lifecycle v1 **Branch**: `292-workspace-tenant-closure` | **Date**: 2026-05-07 | **Spec**: [spec.md](./spec.md) **Input**: Feature specification from `specs/292-workspace-tenant-closure/spec.md` ## Summary Prepare one bounded lifecycle runtime follow-through on top of the Spec 262 taxonomy foundation plus the already-real tenant operability and commercial lifecycle seams. The narrow implementation path is to add explicit workspace closed truth on `Workspace`, explicit removed-from-workspace truth on `ManagedEnvironment`, keep commercial suspension as a separate shared gate, and reuse the existing system directory, admin resource, chooser, and canonical historical viewer surfaces instead of opening a new workflow console. This slice stays deliberately narrow. Filament remains v5 on Livewire v4, panel-provider registration stays in `apps/platform/bootstrap/providers.php`, no new globally searchable resource is introduced, and no asset registration change is expected. The plan does not reopen the broader taxonomy, does not implement purge or export-before-delete, does not add payment-provider or billing-portal behavior, and does not add a new `OperationRun` family. ## Inherited Baseline / Explicit Delta ### Inherited baseline - `Workspace` already carries `archived_at`, and `WorkspaceContext::isWorkspaceSelectable()` already uses that archived truth to prevent invalid workspace selection. - `ManagedEnvironment` already carries tenant lifecycle truth, and `TenantLifecycle` plus `TenantOperabilityService` already distinguish active context from onboarding or archive semantics. - `WorkspaceCommercialLifecycleResolver` already maps commercial truth into `SUSPENDED_READ_ONLY` versus normal workspace posture for high-impact actions. - `EnsureWorkspaceSelected`, `EnsureFilamentTenantSelected`, and `DenyNonMemberTenantAccess` already provide the current context-recovery and deny-as-not-found seams. - `WorkspaceMembershipManager`, `TenantMembershipManager`, `WorkspaceAuditLogger`, and current tenant audit paths already provide bounded audit-safe mutation seams. - Existing admin and system surfaces already exist at `ViewWorkspace`, `ViewTenant`, `ViewRun`, `ChooseWorkspace`, `ChooseTenant`, `WorkspaceResource`, `TenantResource`, and `WorkspaceSettings`. ### Explicit delta in this plan - Add explicit closure truth to `Workspace` using bounded persisted fields for closed posture, actor, and reason. - Add explicit removed-from-workspace truth to `ManagedEnvironment` using bounded persisted fields for removal posture, actor, and reason. - Introduce one bounded `WorkspaceLifecycleService` to orchestrate close/reopen and remove/restore without opening a generic lifecycle framework. - Keep `archived`, `suspended read-only`, `closed`, and `removed from workspace` as four distinct meanings with separate badges, copy, and blocked-action explanations. - Update chooser recovery, tenant-context legitimacy, and canonical historical viewers so historical records remain readable while invalid active context is cleared explicitly. - Keep all new lifecycle mutations inside current admin and system surfaces and current audit infrastructure. ## Technical Context **Language/Version**: PHP 8.4, Laravel 12 **Primary Dependencies**: Filament v5, Livewire v4, Pest v4, existing `WorkspaceContext`, `TenantOperabilityService`, `WorkspaceCommercialLifecycleResolver`, current audit infrastructure, current Filament admin and system pages **Storage**: PostgreSQL via targeted new lifecycle fields on existing `workspaces` and `managed_environments` tables plus existing audit and history tables **Testing**: Pest v4 focused `Feature` coverage **Validation Lanes**: fast-feedback, confidence **Target Platform**: Laravel monolith in `apps/platform` across the existing admin and system Filament panels **Project Type**: Web application (Laravel monolith with Filament panels) **Performance Goals**: DB-only lifecycle gating, no new queue family, no new Graph calls, no new browser-only proof requirement **Constraints**: no purge, no export-before-delete, no payment-provider or billing workflow, no new global-search resource, no new panel, no lifecycle engine, no asset-registration change **Scale/Scope**: 2 existing persisted records gain bounded lifecycle truth, 1 bounded service seam, 6 existing operator surfaces, and focused feature-test extensions ## Likely Affected Repo Surfaces - `apps/platform/app/Models/Workspace.php` - `apps/platform/app/Models/ManagedEnvironment.php` - `apps/platform/database/migrations/*_add_workspace_closure_fields.php` - `apps/platform/database/migrations/*_add_managed_environment_workspace_removal_fields.php` - `apps/platform/app/Services/Workspaces/WorkspaceLifecycleService.php` - `apps/platform/app/Support/Workspaces/WorkspaceContext.php` - `apps/platform/app/Services/Tenants/TenantOperabilityService.php` - `apps/platform/app/Services/Entitlements/WorkspaceCommercialLifecycleResolver.php` - `apps/platform/app/Http/Middleware/EnsureWorkspaceSelected.php` - `apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php` - `apps/platform/app/Support/Middleware/DenyNonMemberTenantAccess.php` - `apps/platform/app/Filament/System/Pages/Directory/ViewWorkspace.php` - `apps/platform/app/Filament/System/Pages/Directory/ViewTenant.php` - `apps/platform/app/Filament/System/Pages/Ops/ViewRun.php` - `apps/platform/app/Filament/Pages/ChooseWorkspace.php` - `apps/platform/app/Filament/Pages/ChooseTenant.php` - `apps/platform/app/Filament/Pages/Settings/WorkspaceSettings.php` - `apps/platform/app/Filament/Resources/Workspaces/WorkspaceResource.php` - `apps/platform/app/Filament/Resources/Workspaces/Pages/ViewWorkspace.php` - `apps/platform/app/Filament/Resources/TenantResource.php` - `apps/platform/app/Services/Audit/WorkspaceAuditLogger.php` - current tenant audit logging path where tenant membership and lifecycle actions are recorded - focused feature tests under `apps/platform/tests/Feature/System/`, `apps/platform/tests/Feature/Filament/Resources/`, and `apps/platform/tests/Feature/Filament/Pages/` ## Lifecycle Truth Fit - Treat workspace closure as a dedicated workspace-owned lifecycle posture, not as archive and not as commercial suspension. - Treat tenant removal from workspace as a dedicated workspace-owned posture on the tenant record, not as tenant archive and not as provider absence. - Keep commercial suspension in `WorkspaceCommercialLifecycleResolver` as the shared source of `SUSPENDED_READ_ONLY`; closure logic must compose with it, not replace it. - Preserve current tenant lifecycle and provider-missing semantics; the feature only adds workspace-governed closure and removal truth. - Preserve historical readability: - closed workspace -> not selectable as current workspace, read-only historical inspection remains available to entitled actors - removed tenant -> not selectable as current tenant, canonical historical inspection remains available to entitled actors - suspended read-only -> current workspace remains inspectable and current tenant may remain visible, but mutations and starts stay blocked per existing commercial rules ## Data & Query Fit - Extend `workspaces` with bounded closure fields only. The expected shape is a timestamp, actor reference, and reason text rather than a broad lifecycle ledger. - Extend `managed_environments` with bounded removed-from-workspace fields only. The expected shape mirrors the workspace closure truth and remains reversible. - Add only the indexes needed to filter active versus closed workspaces and active versus removed tenants. - Keep lifecycle truth on the primary records instead of introducing a new history table. Audit remains the historical source for who changed posture and when. - Preserve existing ownership rules: workspaces stay workspace-owned, tenants stay workspace-owned, and no cross-workspace migration or copy is introduced. ## UI / Filament & Livewire Fit - Existing operator-facing surfaces remain native Filament surfaces under Livewire v4; this slice must stay inside those surfaces. - `ViewWorkspace` in the system plane keeps one dominant action: `Close workspace` or `Reopen workspace`. - Admin workspace detail gains read-only posture summary only; no second mutation plane for closure appears on `/admin`. - Managed tenant list keeps row click as the inspect model and moves remove or restore into `More`; managed tenant detail may expose `Remove tenant` or `Restore tenant` as the dominant lifecycle action. - Chooser surfaces remain chooser surfaces. They gain explicit recovery messaging, not a new workflow. - Canonical historical viewers gain posture badges or supporting text only; they do not become mutation surfaces. - Confirmation modals and success or error notifications on the in-scope action surfaces must reuse the canonical verbs `Close workspace`, `Reopen workspace`, `Remove tenant`, and `Restore tenant`. - No new globally searchable Filament resource is introduced, so there is no new global-search Edit or View page requirement to satisfy. - Provider registration remains unchanged in `apps/platform/bootstrap/providers.php`, and no new asset strategy is planned. If future shared assets ever become necessary, deployment remains the normal `cd apps/platform && php artisan filament:assets` path. ## RBAC / Policy Fit - Workspace and tenant membership remain the isolation boundaries. - Wrong-plane actors and non-members stay `404`; in-scope actors missing capability stay `403`. - Closure and removal actions remain server-side authorized and confirmation-protected. - Platform closure uses the current platform workspace-governance seam or the narrowest bounded extension of it. Tenant removal uses the current workspace owner or equivalent tenant-governance seam. - The exact write-side enforcement seam is `apps/platform/app/Services/Workspaces/WorkspaceLifecycleService.php`, and the in-scope mutation entry points are the system workspace detail actions in `apps/platform/app/Filament/System/Pages/Directory/ViewWorkspace.php` and the tenant lifecycle actions in `apps/platform/app/Filament/Resources/TenantResource.php`. - Unsafe close or remove attempts must preserve current guard failures with a clear operator-facing reason and no partial lifecycle mutation. - Closure or removal posture must never substitute for authorization. A historically readable record still requires entitlement. - Tenant-context routes under `/admin/t/{tenant}/...` must reject removed tenants as active context targets while preserving canonical viewers that are not tenant-context routes. ## Audit / Logging Fit - Every close/reopen and remove/restore mutation must write an audit event with actor, old posture, new posture, timestamp, and reason. - Existing audit infrastructure must be reused rather than opening a new lifecycle audit subsystem. - Historical viewers should surface closure or removal context through current summary and linked audit rather than duplicating the full audit payload. - Blocked active-context starts caused by closure or removal do not need a new audit family in this slice unless existing blocked-action logging already applies. ## UI / Surface Guardrail Plan - **Guardrail scope**: changed surfaces - **Native vs custom classification summary**: native Filament - **Shared-family relevance**: status messaging, chooser recovery, detail actions, canonical historical viewers, audit-backed lifecycle copy - **State layers in scope**: shell, page, detail - **Audience modes in scope**: operator-MSP, support-platform - **Decision/diagnostic/raw hierarchy plan**: decision-first, diagnostics-second, support-raw-third - **Raw/support gating plan**: raw audit and low-level identifiers remain secondary and platform-only where applicable - **One-primary-action / duplicate-truth control**: system workspace detail keeps one dominant closure action; tenant detail keeps one dominant remove/restore action; chooser recovery surfaces keep one next step - **Handling modes by drift class or surface**: review-mandatory - **Repository-signal treatment**: review-mandatory now; hard-stop candidate if implementation adds a second closure plane or local lifecycle vocabulary - **Special surface test profiles**: standard-native-filament, global-context-shell, shared-detail-family - **Required tests or manual smoke**: functional-core, state-contract - **Exception path and spread control**: none planned; any lifecycle dashboard, broad workbench, or browser-heavy proof demand is out-of-scope drift - **Active feature PR close-out entry**: Guardrail ## Shared Pattern & System Fit - **Cross-cutting feature marker**: yes - **Systems touched**: `WorkspaceContext`, `TenantOperabilityService`, `WorkspaceCommercialLifecycleResolver`, chooser pages, system directory pages, admin workspace and tenant resources, audit logging, and canonical historical viewers - **Shared abstractions reused**: current context manager, current commercial lifecycle resolver, current tenant operability service, current audit infrastructure, `BadgeCatalog` and `BadgeRenderer`, current Filament action-surface patterns - **New abstraction introduced? why?**: one bounded `WorkspaceLifecycleService`, because close/reopen and remove/restore each require coordinated record mutation, audit logging, and shared blocked-action consequences across multiple surfaces - **Why the existing abstraction was sufficient or insufficient**: current services already decide selectability, commercial blocking, and audit shape, but no existing service owns the explicit closure or removal mutation path or the shared write-side behavior for it - **Bounded deviation / spread control**: no second closure orchestration helper is allowed outside the bounded service path ## OperationRun UX Impact - **Touches OperationRun start/completion/link UX?**: yes, by reuse and blocking only - **Central contract reused**: current shared `OperationRun` start UX and current canonical monitoring pages remain authoritative - **Delegated UX behaviors**: blocked starts in closed workspaces or removed-tenant contexts must fail before enqueue on `apps/platform/app/Filament/Widgets/Tenant/TenantReviewPackCard.php`; existing run links and canonical historical viewers remain unchanged for already-created runs - **Surface-owned behavior kept local**: close/reopen and remove/restore input collection plus impact summaries only - **Queued DB-notification policy**: unchanged - **Terminal notification path**: unchanged central lifecycle mechanism - **Exception path**: none ## Provider Boundary & Portability Fit - **Shared provider/platform boundary touched?**: no - **Provider-owned seams**: none in this slice - **Platform-core seams**: workspace closure truth, tenant removal truth, chooser recovery, and historical record readability - **Neutral platform terms / contracts preserved**: `workspace`, `tenant`, `closed`, `removed from workspace`, `suspended read-only`, `history` - **Retained provider-specific semantics and why**: none - **Bounded extraction or follow-up path**: provider-missing and other provider lifecycle work stays in separate follow-up specs ## Constitution Check *GATE: Must pass before implementation begins and again before merge.* - Inventory-first / snapshot truth: PASS. No inventory or snapshot source is reinterpreted. - Read/write separation: PASS. The only new writes are explicit lifecycle mutations with confirmation, audit, and test requirements. - Graph contract path: PASS. No Graph calls are introduced. - Deterministic capabilities: PASS. Existing capability registries remain authoritative. - Workspace and tenant isolation: PASS. Existing 404 and 403 rules remain and are reinforced. - RBAC-UX plane separation: PASS. System owns workspace closure mutation; admin remains read-only for workspace closure and bounded for tenant removal inside the current workspace. - Destructive action discipline: PASS. Close/reopen and remove/restore are destructive-like and must remain confirmation-protected. - Global search safety: PASS. No new searchable resource is introduced. - OperationRun / Ops-UX: PASS by reuse only. No new run family is added, and blocked starts stay pre-enqueue. - Data minimization: PASS. Only bounded lifecycle truth fields are added on existing records. - Test governance: PASS. Focused feature lanes are the narrowest honest proof. - Proportionality / no premature abstraction: PASS. One bounded service is the narrowest write-side seam; no generic lifecycle framework is planned. - Persisted truth: PASS. Closure and removal posture are real product truth with independent lifecycle and audit need. - Behavioral state: PASS. Closed and removed-from-workspace change chooser behavior, mutation legality, and historical-view posture. - Shared pattern first / UI semantics / Filament-native UI: PASS. Existing chooser, audit, badge, and Filament surfaces remain the shared path. - Provider boundary: PASS. No provider-specific semantics are added. - Filament / Laravel planning contract: PASS. Filament stays v5 on Livewire v4, provider registration remains in `apps/platform/bootstrap/providers.php`, no globally searchable resource is added, and no asset registration change is planned. **Gate evaluation**: PASS. ## Test Governance Check - **Test purpose / classification by changed surface**: `Feature` for system workspace governance, admin read-only posture, managed-tenant remove/restore, chooser recovery, and canonical historical viewer legitimacy - **Affected validation lanes**: fast-feedback, confidence - **Why this lane mix is the narrowest sufficient proof**: the slice is about integrated route, page, capability, chooser, and audit behavior; unit-only proof would miss the real lifecycle consequences - **Narrowest proving command(s)**: - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/System/Directory/ViewWorkspaceClosureTest.php tests/Feature/System/Ops/ClosedWorkspaceHistoricalAccessTest.php` - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/Resources/Workspaces/WorkspaceClosureStatusTest.php tests/Feature/Filament/Resources/TenantResource/TenantWorkspaceRemovalTest.php tests/Feature/Filament/Pages/WorkspaceContextClosureRecoveryTest.php` - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - **Fixture / helper / factory / seed / context cost risks**: moderate but contained; setup needs explicit workspace and tenant membership plus lifecycle posture states, but no new provider, browser, or heavy defaults - **Expensive defaults or shared helper growth introduced?**: no - **Heavy-family additions, promotions, or visibility changes**: none planned - **Surface-class relief / special coverage rule**: standard-native-filament relief for the system and admin detail surfaces; global-context-shell coverage for chooser recovery; shared-detail-family coverage for historical viewers - **Closing validation and reviewer handoff**: reviewers should rely on the exact commands above, verify that no second mutation plane or lifecycle vocabulary appeared, and confirm that blocked starts stay pre-enqueue while historical viewers stay readable - **Budget / baseline / trend follow-up**: none expected beyond a small feature-local feature-test increase - **Review-stop questions**: did the slice collapse closure into archive or suspension, did it break canonical history viewers, did it add a second mutation plane, and did it widen into purge/export/billing scope - **Escalation path**: `document-in-feature` for contained naming or surface drift; `reject-or-split` if the slice widens into broader offboarding, purge, or billing workflows - **Active feature PR close-out entry**: Guardrail - **Why no dedicated follow-up spec is needed**: the remaining adjacent work is already known and separately named; this slice is the bounded workspace and tenant closure follow-through itself - **Test-governance outcome**: keep ## Rollout Considerations - Land persistence and the bounded service seam first, then workspace closure surfaces plus chooser recovery, then tenant removal surfaces plus canonical historical viewer polish. - Keep admin workspace detail read-only for closure mutation throughout the slice. - Keep commercial suspension readable and separate from closure throughout the rollout. - Keep no asset changes, no provider registration changes, no new panel work, and no global-search work. ## Risk Controls - Reject any implementation that reuses `archived_at` or commercial suspension as the new closure truth. - Reject any implementation that deletes memberships, tenants, workspaces, or historical records as part of close/reopen or remove/restore. - Reject any implementation that makes `/admin` a second workspace-closure mutation plane. - Reject any implementation that turns the slice into export, purge, payment-provider, or portal work. - Reject browser-heavy proof as the default validation lane. ## Project Structure ### Documentation (this feature) ```text specs/292-workspace-tenant-closure/ ├── plan.md ├── spec.md └── tasks.md ``` ### Source Code (expected implementation surfaces) ```text apps/platform/ ├── app/ │ ├── Filament/ │ │ ├── Pages/ │ │ │ ├── ChooseTenant.php │ │ │ ├── ChooseWorkspace.php │ │ │ └── Settings/ │ │ │ └── WorkspaceSettings.php │ │ ├── Resources/ │ │ │ ├── TenantResource.php │ │ │ └── Workspaces/ │ │ │ ├── WorkspaceResource.php │ │ │ └── Pages/ │ │ │ └── ViewWorkspace.php │ │ └── System/Pages/ │ │ ├── Directory/ │ │ │ ├── ViewTenant.php │ │ │ └── ViewWorkspace.php │ │ └── Ops/ │ │ └── ViewRun.php │ ├── Http/Middleware/ │ │ └── EnsureWorkspaceSelected.php │ ├── Models/ │ │ ├── ManagedEnvironment.php │ │ └── Workspace.php │ ├── Services/ │ │ ├── Audit/ │ │ │ └── WorkspaceAuditLogger.php │ │ ├── Entitlements/ │ │ │ └── WorkspaceCommercialLifecycleResolver.php │ │ ├── Tenants/ │ │ │ └── TenantOperabilityService.php │ │ └── Workspaces/ │ │ └── WorkspaceLifecycleService.php │ └── Support/ │ ├── Middleware/ │ │ ├── DenyNonMemberTenantAccess.php │ │ └── EnsureFilamentTenantSelected.php │ └── Workspaces/ │ └── WorkspaceContext.php ├── database/ │ └── migrations/ └── tests/ └── Feature/ ├── Filament/ └── System/ ``` **Structure Decision**: Keep the slice inside the existing Laravel monolith and current Filament admin plus system surfaces. Add only targeted lifecycle fields, one bounded service, and focused feature tests. ## Complexity Tracking | Violation | Why Needed | Simpler Alternative Rejected Because | |-----------|------------|-------------------------------------| | Bounded lifecycle write service | Close/reopen and remove/restore need coordinated mutation, audit, and shared blocked-action behavior | Controller or page-local closures would duplicate audit and lifecycle consequences across multiple surfaces | ## Proportionality Review - **Current operator problem**: Operators cannot deliberately close a workspace or remove a tenant from a workspace while keeping history readable and posture explicit. - **Existing structure is insufficient because**: archive and commercial suspension represent different meanings and cannot safely absorb closure or removal truth. - **Narrowest correct implementation**: add bounded fields on existing records, route writes through one bounded service, and reuse current chooser, audit, and Filament surfaces. - **Ownership cost created**: migrations, one service, shared status and chooser updates, and feature tests. - **Alternative intentionally rejected**: generic lifecycle engine, new closure history tables, or overloading archive and suspension. - **Release truth**: current-release truth.