# Implementation Plan: Spec 093 — SCOPE-001 Workspace ID Isolation **Branch**: `093-scope-001-workspace-id-isolation` | **Date**: 2026-02-14 **Spec**: `specs/093-scope-001-workspace-id-isolation/spec.md` **Spec (absolute)**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/093-scope-001-workspace-id-isolation/spec.md` **Input**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/093-scope-001-workspace-id-isolation/spec.md` ## Summary Enforce DB-level workspace isolation for tenant-owned data by adding `workspace_id` to 12 tenant-owned tables, safely backfilling legacy rows, and then enforcing NOT NULL + referential integrity. Additionally, fix the audit trail invariant: if an `audit_logs` entry references a tenant, it must also reference a workspace. Rollout is staged to avoid downtime: 1) Add nullable `workspace_id` columns. 2) Enforce write-path derivation + mismatch rejection. 3) Backfill in batches with resumability, locking, and observability (`OperationRun` + `AuditLog`). 4) Enforce constraints and add final indexes. ## Technical Context **Language/Version**: PHP 8.4 (Laravel 12) **Primary Dependencies**: Filament v5, Livewire v4, Laravel Sail, Tailwind CSS v4 **Storage**: PostgreSQL (primary), with SQLite support patterns used in migrations for tests/CI **Testing**: Pest v4 (`vendor/bin/sail artisan test --compact`) **Target Platform**: Web (admin SaaS) **Project Type**: Laravel monolith (Filament panels + Livewire + Artisan commands) **Performance Goals**: - Backfill updates run in batches to avoid long locks. - Postgres uses `CONCURRENTLY` for large index creation where applicable. **Constraints**: - No new HTTP routes/pages. - No planned downtime; staged rollout. - Backfill is idempotent, resumable, and aborts on tenant→workspace mapping failures. **Scale/Scope**: Potentially large datasets (unknown upper bound); plan assumes millions of rows are possible across inventory/backup/history tables. ## Constitution Check *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* - Inventory-first / snapshots: PASS (schema-only + backfill; no changes to inventory/snapshot semantics). - Read/write separation: PASS (writes are limited to migrations + operator backfill; no UI “write surfaces” are added). - Graph contract path: PASS (no Graph calls). - Deterministic capabilities: PASS (no capability resolver changes). - Workspace isolation: PASS (strengthens isolation by enforcing workspace binding at the data layer). - Tenant isolation: PASS (tenant-owned tables remain tenant-scoped; DB constraints prevent cross-workspace mismatches). - RBAC-UX / planes: PASS (no changes to `/admin` vs `/system`; no new access surfaces). - Run observability: PASS (backfill is operationally relevant and will be tracked via `OperationRun` + `AuditLog`). - Filament Action Surface Contract: N/A (no Filament Resource/Page changes). ## Project Structure ### Documentation (this feature) ```text specs/093-scope-001-workspace-id-isolation/ ├── plan.md ├── research.md ├── data-model.md ├── quickstart.md ├── contracts/ │ ├── openapi.yaml │ └── cli.md └── tasks.md ``` ### Source Code (repository root) ```text app/ ├── Console/ │ └── Commands/ ├── Models/ └── Support/ (or Services/) database/ └── migrations/ tests/ └── Feature/ ``` **Structure Decision**: Implement as Laravel migrations + an Artisan operator command + model-level enforcement helpers, with Pest feature tests. ## Phase Plan ### Phase 0 — Research (complete) Outputs: - `specs/093-scope-001-workspace-id-isolation/research.md` Key decisions captured: - Tenant↔workspace consistency will be enforced with composite FKs on Postgres/MySQL. - Audit invariant enforced with a check constraint. ### Phase 1 — Design & Contracts (complete) Outputs: - `specs/093-scope-001-workspace-id-isolation/data-model.md` - `specs/093-scope-001-workspace-id-isolation/contracts/openapi.yaml` (no new routes) - `specs/093-scope-001-workspace-id-isolation/contracts/cli.md` (Artisan backfill contract) - `specs/093-scope-001-workspace-id-isolation/quickstart.md` **Post-design constitution re-check**: PASS (no new external calls; operational backfill is observable). ### Phase 2 — Implementation Planning (next) Implementation will be delivered as small, test-driven slices aligned to the staged rollout. 1) Phase 1 migrations — add nullable `workspace_id` - Add `workspace_id` (nullable) + index to the 12 tenant-owned tables. - Add baseline scoping indexes for expected query patterns (at minimum `workspace_id` and `(workspace_id, tenant_id)` where useful). - Ensure migrations follow existing multi-driver patterns (SQLite fallbacks where needed). 2) Phase 1.5 — write-path enforcement (application) - For each affected model/write path: - On create: derive `workspace_id` from `tenant.workspace_id`. - On update: reject changes to `tenant_id` (immutability) and reject explicit workspace mismatches. - Ensure audit log writer sets `workspace_id` when `tenant_id` is present. 3) Phase 2 — backfill command (operator-only) - Add `tenantpilot:backfill-workspace-ids`. - Safety requirements: - Acquire lock to prevent concurrent execution. - Batch updates per table and allow resume/checkpoint. - Abort and report table + sample IDs if a tenant→workspace mapping cannot be resolved. - Observability: - Create/reuse an `OperationRun` describing the backfill run. - Write `AuditLog` summary entries for start/end/outcome. - Execution strategy (queued): - The command MUST be a lightweight start surface: authorize → acquire lock → create/reuse OperationRun → dispatch queued jobs → print a “View run” pointer. - The actual backfill mutations MUST execute inside queued jobs (batch/table scoped) so large datasets do not require a single long-running synchronous CLI process. - Implementation maps to `app/Console/Commands/TenantpilotBackfillWorkspaceIds.php` + `app/Jobs/BackfillWorkspaceIdsJob.php`. - Jobs MUST update OperationRun progress/counters and record failures with stable reason codes + sanitized messages. 4) Phase 3 — constraints + validation + final indexes - Tenant-owned tables: - Set `workspace_id` to NOT NULL (after validation). - Add FK `workspace_id → workspaces.id`. - Add composite FK `(tenant_id, workspace_id) → tenants(id, workspace_id)` on Postgres/MySQL. - For Postgres, prefer `NOT VALID` then `VALIDATE CONSTRAINT` to reduce lock time. - Tenants: - Add a unique constraint/index on `(id, workspace_id)` to support composite FKs. - Audit logs: - Backfill `workspace_id` for rows where `tenant_id` is present. - Add check constraint: `tenant_id IS NULL OR workspace_id IS NOT NULL`. - Index strategy: - Use `CREATE INDEX CONCURRENTLY` on Postgres for large tables (migrations must not run in a transaction). 5) Pest tests (minimal, high-signal) - Backfill correctness on a representative table (seed missing `workspace_id`, run backfill, assert set). - DB constraint tests (where supported by test DB): - `tenant_id` + mismatched `workspace_id` cannot be persisted after Phase 3 constraints. - audit invariant: tenant-scoped audit requires workspace; workspace-only and platform-only are allowed.