# Data Model: Workspace Chooser v1 **Feature**: 107-workspace-chooser | **Date**: 2026-02-22 ## Existing Entities (No Changes) ### workspaces | Column | Type | Nullable | Notes | |--------|------|----------|-------| | id | bigint (PK) | NO | Auto-increment | | name | varchar(255) | NO | Display name | | slug | varchar(255) | YES | URL-safe identifier | | archived_at | timestamp | YES | Soft-archive marker; non-null = archived | | created_at | timestamp | NO | | | updated_at | timestamp | NO | | ### workspace_memberships | Column | Type | Nullable | Notes | |--------|------|----------|-------| | id | bigint (PK) | NO | Auto-increment | | workspace_id | bigint (FK) | NO | → workspaces.id | | user_id | bigint (FK) | NO | → users.id | | role | varchar(255) | NO | 'owner', 'admin', 'member' | | created_at | timestamp | NO | | | updated_at | timestamp | NO | | ### users (relevant columns only) | Column | Type | Nullable | Notes | |--------|------|----------|-------| | last_workspace_id | bigint (FK) | YES | → workspaces.id; auto-resume preference | ### audit_logs (relevant columns only) | Column | Type | Nullable | Notes | |--------|------|----------|-------| | id | bigint (PK) | NO | | | workspace_id | bigint | YES | → workspaces.id | | tenant_id | bigint | YES | NULL for workspace-scoped events | | actor_id | bigint | YES | → users.id | | actor_email | varchar | YES | | | actor_name | varchar | YES | | | action | varchar | NO | stable action ID string | | resource_type | varchar | YES | | | resource_id | varchar | YES | | | status | varchar | NO | 'success' / 'failure' | | metadata | jsonb | YES | additional context (sanitized) | | recorded_at | timestamp | NO | | ### Session (in-memory / store-backed) | Key | Type | Notes | |-----|------|-------| | `current_workspace_id` | int | Set by `WorkspaceContext::setCurrentWorkspace()` | ## New Data (Enum Values Only) ### AuditActionId Enum — New Cases ```php case WorkspaceAutoSelected = 'workspace.auto_selected'; case WorkspaceSelected = 'workspace.selected'; ``` ### Audit Log Metadata Schema (for workspace selection events) ```jsonc { "method": "auto" | "manual", "reason": "single_membership" | "last_used" | "chooser" | "context_bar", "prev_workspace_id": 123 | null // previous workspace if switching } ``` ## Entity Relationships (relevant to this feature) ```text User ──< WorkspaceMembership >── Workspace │ │ └── last_workspace_id ────────────┘ User ──< AuditLog >── Workspace ``` ## Validation Rules | Field | Rule | Source | |-------|------|--------| | Workspace selectability | `archived_at IS NULL` | `WorkspaceContext::isWorkspaceSelectable()` | | Membership check | `workspace_memberships WHERE user_id AND workspace_id` | `WorkspaceContext::isMember()` | | `choose` param | `?choose=1` (truthy string) | Middleware step 2 | | Non-member selection attempt | abort(404) | FR deny-as-not-found | ## State Transitions The workspace selection flow is a session-context transition, not a data state machine. ```text [No Session] ──auto-resume──> [Active Workspace Session] [No Session] ──chooser──────> [Active Workspace Session] [Active Session] ──switch───> [Active Workspace Session (different)] [Active Session] ──revoked──> [No Session] + warning flash [Active Session] ──archived─> [No Session] + warning flash ``` ## Query Patterns ### Chooser Page Query (FR-003, FR-011) ```php Workspace::query() ->whereIn('id', function ($query) use ($user) { $query->from('workspace_memberships') ->select('workspace_id') ->where('user_id', $user->getKey()); }) ->whereNull('archived_at') ->withCount('tenants') ->orderBy('name') ->get(); ``` Joined via subquery (no N+1). `withCount('tenants')` adds a single correlated subquery. Result includes `tenants_count` attribute. ### Role Retrieval for Display ```php // Eager-load membership pivot to get role per workspace // Option A: Join workspace_memberships in the query // Option B: Use $workspace->pivot->role when loaded via user relationship // Preferred: load memberships separately keyed by workspace_id $memberships = WorkspaceMembership::query() ->where('user_id', $user->getKey()) ->pluck('role', 'workspace_id'); // Then in view: $memberships[$workspace->id] ?? 'member' ``` Single query, keyed by workspace_id, accessed in O(1) per card.