82 lines
1.8 KiB
Markdown
82 lines
1.8 KiB
Markdown
# 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`.
|