# Research: Workspace Chooser v1 **Feature**: 107-workspace-chooser | **Date**: 2026-02-22 ## R1: Middleware Refactor Strategy **Question**: Should we create a new `EnsureActiveWorkspace` middleware or refactor the existing `EnsureWorkspaceSelected`? - **Decision**: Refactor the existing `EnsureWorkspaceSelected` middleware in-place. - **Rationale**: The existing middleware is already registered in both `AdminPanelProvider` and `TenantPanelProvider` middleware stacks as `'ensure-workspace-selected'`, and referenced by alias in `bootstrap/app.php`. Creating a new class would require changing all registration points and updating existing tests. The current class already handles the same responsibilities — it just doesn't implement them according to the spec. - **Alternatives considered**: - New `EnsureActiveWorkspace` class: rejected because it would require renaming the middleware alias everywhere, with no functional benefit beyond a name change. The alias can remain `ensure-workspace-selected` for backward compatibility. ## R2: Audit Event Integration Pattern **Question**: How should workspace selection audit events be emitted? - **Decision**: Call `WorkspaceAuditLogger::log()` directly from the middleware (for auto-selections) and from `ChooseWorkspace::selectWorkspace()` (for manual selections). No events/listeners needed. - **Rationale**: `WorkspaceAuditLogger` is a simple synchronous service — no queue, no listener. The codebase pattern (used in workspace membership, settings, alert destinations, baselines, etc.) is direct `$logger->log(...)` calls at the mutation point. Workspace selection audit is similarly < 1ms DB insert. - **Alternatives considered**: - Laravel Events + Listeners: rejected — overkill for a synchronous log write. No other systems need to react to workspace selection events. - Observer on `User` model (`last_workspace_id` change): rejected — would miss cases where only the session changes (auto-resume from session), and would conflate preference persistence with audit semantics. ## R3: `AuditActionId` Enum Values **Question**: What enum values and string representations to use? - **Decision**: Add two cases: - `WorkspaceAutoSelected = 'workspace.auto_selected'` — for auto-resume (single membership or last-used). - `WorkspaceSelected = 'workspace.selected'` — for manual selection from chooser. - **Rationale**: Follows the existing naming pattern (`case CamelName = 'snake.dotted_value'`). The `method` (auto/manual) and `reason` (single_membership/last_used/chooser) are stored in the audit log's `metadata` JSONB, not in separate enum values. - **Alternatives considered**: - Three separate enum values (one per reason): rejected — metadata provides sufficient granularity; enum values should represent the action type, not the trigger. ## R4: Redirect After Selection (Tenant-Count Branching) **Question**: Where does redirect logic live? Should it be deduplicated? - **Decision**: Extract the tenant-count branching logic into a shared helper method on `WorkspaceContext` or a dedicated `WorkspaceRedirectResolver` to avoid duplicating it across: 1. `EnsureWorkspaceSelected` middleware (auto-resume redirects) 2. `ChooseWorkspace::selectWorkspace()` (manual selection redirect) 3. `SwitchWorkspaceController::__invoke()` (POST switch redirect) 4. `routes/web.php` `/admin` route handler Currently, the same branching logic (0 tenants → managed-tenants, 1 → tenant dashboard, >1 → choose-tenant) is copy-pasted in all four locations. - **Rationale**: DRY — the branching is identical in all cases and is the single authority for "where to go after workspace is set." A single method eliminates the risk of divergence as new conditions are added. - **Alternatives considered**: - Leave duplicated: rejected — 4 copies of the same logic is a maintenance hazard. - Put on `ChooseWorkspace` page: rejected — the middleware and controller both need it but don't have access to the page class. ## R5: `?choose=1` Handling Location **Question**: Should the `?choose=1` forced-chooser parameter be handled in the middleware or in the page? - **Decision**: Handle in the middleware — step 2 of the algorithm. If `choose=1` is present, redirect to `/admin/choose-workspace?choose=1` and skip auto-resume logic. - **Rationale**: The middleware is the single entry point for all `/admin/*` requests. Handling it there prevents auto-resume from overriding the explicit user intent to see the chooser. - **Alternatives considered**: - Handle in `ChooseWorkspace` page: rejected — the middleware would auto-resume BEFORE the page loads, so the user would never see the chooser. ## R6: User Menu Integration **Question**: How to add "Switch workspace" to the Filament user menu? - **Decision**: Register via `->userMenuItems()` in `AdminPanelProvider::panel()`. Use `Filament\Navigation\MenuItem::make()` with `->url('/admin/choose-workspace?choose=1')` and a `->visible()` callback that checks workspace membership count > 1. - **Rationale**: This is the documented Filament v5 pattern from the constitution + blueprint. The menu item is a navigation-only action (URL link), not a destructive action, so no confirmation needed. - **Alternatives considered**: - Context bar link only: rejected — specification explicitly requires a user menu entry (FR-008). - Adding to both user menu and context bar: the context bar already has "Switch workspace" — the user menu entry provides an additional discovery point per spec. ## R7: Badge Rendering for Workspace Role **Question**: Should workspace membership role badges use `BadgeCatalog`/`BadgeDomain`? - **Decision**: No. Workspace membership role is a **tag/category** (owner, admin, member), not a status-like value. Per constitution (BADGE-001), tag/category chips are not governed by `BadgeCatalog`. Use a simple Filament `` with a color mapping (e.g., owner → primary, admin → warning, member → gray). - **Rationale**: The role is static metadata, not a state transition. No existing `BadgeDomain` for workspace roles. Adding one would be over-engineering for 3 static values. - **Alternatives considered**: - Create a `WorkspaceRoleBadgeDomain`: rejected — violates the "tag, not status" exemption in BADGE-001. ## R8: Blade Template vs. Livewire Component for Chooser **Question**: Should the chooser cards remain as a Blade template or be converted to a Livewire component? - **Decision**: Keep as Blade template rendered by the existing Livewire-backed `ChooseWorkspace` Filament Page. The page class already extends `Filament\Pages\Page` (which is Livewire). The `wire:click` for "Open" calls the existing `selectWorkspace()` method. No separate Livewire component needed. - **Rationale**: The existing pattern works. The page is already a Livewire component (all Filament Pages are). Converting to a separate Livewire component adds complexity with no benefit — the chooser has no real-time reactive needs. - **Alternatives considered**: - Separate `WorkspaceChooserCard` Livewire component: rejected — unnecessary abstraction for a simple card grid. ## R9: Existing `SwitchWorkspaceController` Coexistence **Question**: The chooser currently uses POST to `SwitchWorkspaceController`. Should we switch to Livewire `wire:click` or keep the POST? - **Decision**: Migrate the chooser page to use Livewire `wire:click` calling `selectWorkspace($workspaceId)` (which already exists). The `SwitchWorkspaceController` is still needed for the `workspace-switcher.blade.php` partial (context-bar dropdown) which uses a form POST. Both paths converge on `WorkspaceContext::setCurrentWorkspace()`. - **Rationale**: The Livewire path already exists in `ChooseWorkspace::selectWorkspace()` — the blade template just needs to call it via `wire:click` instead of a form POST. This simplifies the chooser page and makes audit integration easier (audit logging happens in the PHP method, not in a controller). - **Alternatives considered**: - Keep form POST from chooser: rejected — the `selectWorkspace()` method is where we add audit logging. Using `wire:click` means a single code path. - Remove `SwitchWorkspaceController` entirely: deferred — the context-bar dropdown still uses it. Can be unified in a future PR. ## R10: Flash Warning Implementation **Question**: How to show "Your access to {workspace_name} was removed" when stale membership detected? - **Decision**: Use Filament database notifications or session flash + `Filament\Notifications\Notification::make()->danger()`. Since the middleware redirects to the chooser, and the chooser is a Filament page, Filament's notification system renders flash/database notifications automatically. - **Rationale**: The chooser is a Filament page — Filament's notification toast system is already wired. Session-based `Notification::make()` works for redirect→page scenarios. - **Alternatives considered**: - Custom Blade banner: rejected — Filament notifications already solve this and are consistent with the rest of the app. - Session flash only (no Filament notification): rejected — the Filament notification system provides better UX (auto-dismiss, consistent styling).