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