# Data Model — Managed Tenant Onboarding Wizard V1 (Enterprise) (073)

## Entities

### Workspace

Existing entity: `App\Models\Workspace`

- Onboarding is always initiated within a selected workspace context.
- Workspace membership is the primary isolation boundary for wizard + tenantless operations viewing.

### Tenant (Managed Tenant)

Existing model: `App\Models\Tenant`

**Key fields (existing or to extend):**

- `id` (PK)
- `workspace_id` (FK → workspaces)
- `tenant_id` (string; Entra Tenant ID) — spec’s `entra_tenant_id` (globally unique)
- `external_id` (string; Filament tenant route key; currently used in `/admin/t/{tenant}`)
- `name` (string)
- `primary_domain` (string|null)
- `notes` (text|null)
- `environment` (string)
- `status` (string) — v1 lifecycle:
  - `draft`
  - `onboarding`
  - `active`
  - `archived`

**Indexes / constraints (design intent):**

- Unique: `tenant_id` (global uniqueness; binds the tenant to exactly one workspace)
- `external_id` must remain globally unique for Filament tenancy routing

**State transitions:**

- `draft` → `onboarding` after identification is recorded
- `onboarding` → `active` on owner activation
- `active` → `archived` via archive/deactivate workflow

### Provider Connection

Existing model today: `App\Models\ProviderConnection` (currently tenant-owned)

**Spec-aligned ownership model (design intent):**

- Provider connections are workspace-owned.
- Default binding: provider connection bound to exactly one managed tenant.
- Reuse across managed tenants is disabled by default and policy-gated.

**Proposed key fields (target):**

- `id` (PK)
- `workspace_id` (FK → workspaces)
- `managed_tenant_id` (FK → tenants.id; required in v1 default binding)
- `provider` (string)
- `entra_tenant_id` (string)
- `is_default` (bool)
- `metadata` (json)

### Tenant Onboarding Session (new)

New model/table to persist resumable onboarding state for a workspace + Entra Tenant ID.
Must never persist secrets and must render DB-only.

**Proposed fields:**

- `id` (PK)
- `workspace_id` (FK)
- `managed_tenant_id` (FK → tenants.id; nullable until tenant is created)
- `entra_tenant_id` (string; denormalized identity key; globally unique across the system but still stored for idempotency)
- `current_step` (string; `identify`, `connection`, `verify`, `bootstrap`, `complete`)
- `state` (jsonb) — safe fields only (no secrets)
  - `tenant_name`
  - `environment`
  - `primary_domain`
  - `notes`
  - `selected_provider_connection_id`
  - `verification_run_id` (OperationRun id)
  - `bootstrap_run_ids` (array)
- `started_by_user_id` (FK users)
- `updated_by_user_id` (FK users)
- `completed_at` (timestamp|null)
- timestamps

**Constraints:**

- Unique: `entra_tenant_id` (global uniqueness) OR (if sessions are separate from tenants) unique `(workspace_id, entra_tenant_id)` with an additional global “tenant exists elsewhere” guard to enforce deny-as-not-found.

### Operation Run

Existing model: `App\Models\OperationRun`

**Spec-aligned visibility model (design intent):**

- Runs are viewable tenantlessly at `/admin/operations/{run}`.
- Access is granted only to members of the run’s workspace; non-member → deny-as-not-found (404).

**Proposed schema changes:**

- Add `workspace_id` (FK → workspaces), required.
- Allow `tenant_id` to be nullable for pre-activation runs.
- Maintain DB-level active-run idempotency:
  - `UNIQUE (tenant_id, run_identity_hash) WHERE tenant_id IS NOT NULL AND status IN ('queued', 'running')`
  - `UNIQUE (workspace_id, run_identity_hash) WHERE tenant_id IS NULL AND status IN ('queued', 'running')`

## Validation rules (high level)

- `entra_tenant_id`: required, non-empty, validate GUID format.
- Tenant identification requires: `name`, `environment`, `entra_tenant_id`.
- Provider connection selected/created must be in the same workspace.
- Onboarding session `state` must be strictly whitelisted fields (no secrets).

## Authorization boundaries

- Workspace membership boundary: non-member → 404 (deny-as-not-found) for onboarding and tenantless operations run viewing.
- Capability boundary (within membership): action attempts without capability → 403.
- Owner-only boundary: activation and blocked override require workspace owner; override requires reason + audit.
