# Data Model: Workspace Model, Memberships & Managed Tenants (v2) **Date**: 2026-01-31 **Feature**: [spec.md](spec.md) ## Entities ### Workspace Represents an organization/customer scope. **Fields** - `id` (bigint, PK) - `name` (string, required) - `slug` (string, nullable, unique) - `status` (enum/string; values: `active`, `archived`; default `active`) - `created_at`, `updated_at` **Relationships** - hasMany `WorkspaceMembership` - hasMany `ManagedTenant` **Validation / invariants** - `name` required and non-empty - `slug` unique when present - archived workspaces cannot be selected as current workspace --- ### WorkspaceMembership Associates a user to a workspace with one role. **Fields** - `id` (bigint, PK) - `workspace_id` (FK workspaces) - `user_id` (FK users) - `role` (enum/string; values: `owner`, `manager`, `operator`, `readonly`) - `created_at`, `updated_at` **Constraints** - Unique(`workspace_id`, `user_id`) **Invariants** - Last-owner guard: there must always be at least one `owner` membership per workspace. --- ### ManagedTenant A Microsoft Entra/Intune tenant managed inside exactly one Workspace. **Fields (v2 additions)** - `workspace_id` (FK workspaces) - `entra_tenant_id` (string) **Constraints** - Unique(`entra_tenant_id`) globally - Index(`workspace_id`) --- ### User Existing identity row. **Fields (v2 additions)** - `last_workspace_id` (nullable FK workspaces) **Relationships** - hasMany `WorkspaceMembership` ## State transitions - Workspace `status`: `active` → `archived` (archived should not be selectable as current workspace) - Membership role changes: `owner|manager|operator|readonly` with server-side last-owner guard ## Notes - URL identity uses `slug` when present, else `id`. - Current workspace context is session-backed with a DB fallback via `users.last_workspace_id`.