9.7 KiB
Implementation Plan: Workspace Model, Memberships & Managed Tenants (v2)
Branch: 068-workspaces-v2 | Date: 2026-01-31 | Spec: spec.md
Input: Feature specification from spec.md
Note: This template is filled in by the /speckit.plan command. See .specify/scripts/ for helper scripts.
Summary
Introduce a Workspace hierarchy as the primary scope for the tenant-plane admin panel:
- Workspace-scoped routing (
/admin/w/{workspace}/...) with deny-as-not-found (404 semantics) for non-members. - Workspace memberships (owner/manager/operator/readonly) as the source of truth for access and capability derivation.
- Workspace switcher + choose-workspace/no-access entry flows with session +
users.last_workspace_idpersistence. - Managed Tenants become children of Workspaces; onboarding entry becomes canonical under Workspace scope.
- Global search becomes workspace-safe (results only within current workspace; no leakage).
If the repository also contains tenant-scoped routes (e.g. /admin/t/{tenant}/...), they must be made workspace-safe by ensuring the tenant belongs to the currently selected workspace (or by denying-as-not-found).
Design decisions and rationale are captured in research.md.
Technical Context
Language/Version: PHP 8.4.15
Primary Dependencies: Laravel 12, Filament v5, Livewire v4, Tailwind CSS v4
Storage: PostgreSQL (via Sail)
Testing: Pest v4 (+ PHPUnit v12 underneath)
Target Platform: Web application; local dev in Laravel Sail; deploy via containers (Dokploy)
Project Type: Web application (Laravel monolith)
Performance Goals: Admin UX: workspace-scoped pages should remain responsive (target < 200ms p95 for DB-only requests on typical datasets).
Constraints:
- Sail-first execution for all PHP/Artisan/Composer/Node commands.
- No full provider refactor; keep changes scoped to Workspace model, routing/scope, memberships, and managed tenant scoping.
- Preserve deny-as-not-found semantics for non-members; do not leak via global search. Scale/Scope: Multi-workspace, multi-tenant-plane: 1 user may belong to many workspaces; each workspace may contain multiple managed tenants.
Constitution Check
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.
- Inventory-first: clarify what is “last observed” vs snapshots/backups
- Read/write separation: any writes require preview + confirmation + audit + tests
- Graph contract path: Graph calls only via
GraphClientInterface+config/graph_contracts.php - Deterministic capabilities: capability derivation is testable (snapshot/golden tests)
- RBAC-UX: two planes (/admin vs /system) remain separated; cross-plane is 404; non-member tenant access is 404; member-but-missing-capability is 403; authorization checks use Gates/Policies + capability registries (no raw strings, no role-string checks)
- RBAC-UX: destructive-like actions require
->requiresConfirmation()and clear warning text - RBAC-UX: global search is tenant-scoped; non-members get no hints; inaccessible results are treated as not found (404 semantics)
- Tenant isolation: all reads/writes tenant-scoped; cross-tenant views are explicit and access-checked
- Run observability: long-running/remote/queued work creates/reuses
OperationRun; start surfaces enqueue-only; Monitoring is DB-only; DB-only <2s actions may skip runs but security-relevant ones still audit-log; auth handshake exception OPS-EX-AUTH-001 allows synchronous outbound HTTP on/auth/*withoutOperationRun - Automation: queued/scheduled ops use locks + idempotency; handle 429/503 with backoff+jitter
- Data minimization: Inventory stores metadata + whitelisted meta; logs contain no secrets/tokens
- Badge semantics (BADGE-001): status-like badges use
BadgeCatalog/BadgeRenderer; no ad-hoc mappings; new values include tests
Gate evaluation (pre-Phase 0): PASS (with notes)
- Inventory-first: Not directly affected (workspace + membership + routing).
- Read/write separation: Workspace/membership mutations are DB-only; require confirmation for destructive-like actions (remove member / demote last owner attempt), emit audit entries, and add tests.
- Graph contract path: No new Graph calls introduced.
- Deterministic capabilities: Extend the canonical capability registry with workspace-plane capabilities; implement deterministic role → capability mapping and test it.
- RBAC-UX: Apply the same semantics with Workspace scope:
- Non-member workspace scope → deny-as-not-found.
- Member missing capability → forbidden.
- UI disabled vs hidden rules remain.
- Global search safety: Introduce workspace-scoped global search query behavior (no results outside current workspace).
- Run observability: Not applicable (no long-running operations planned). Membership changes still require audit logs.
- Badge semantics: Not expected to change for this feature.
Project Structure
Documentation (this feature)
specs/[###-feature]/
├── plan.md # This file (/speckit.plan command output)
├── research.md # Phase 0 output (/speckit.plan command)
├── data-model.md # Phase 1 output (/speckit.plan command)
├── quickstart.md # Phase 1 output (/speckit.plan command)
├── contracts/ # Phase 1 output (/speckit.plan command)
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
Source Code (repository root)
app/
├── Filament/
│ ├── Resources/
│ │ ├── WorkspaceResource.php
│ │ └── ...
│ └── Pages/
│ ├── ChooseWorkspace.php
│ └── NoAccess.php
├── Http/
│ └── Middleware/
│ ├── EnsureWorkspaceSelected.php
│ └── EnsureWorkspaceMember.php
├── Models/
│ ├── Workspace.php
│ ├── WorkspaceMembership.php
│ └── Tenant.php (existing; updated to belong to Workspace)
├── Policies/
│ ├── WorkspacePolicy.php
│ └── WorkspaceMembershipPolicy.php
├── Support/
│ ├── Auth/
│ │ ├── Capabilities.php (extend with workspace-plane capabilities)
│ │ └── WorkspaceRole.php (new enum)
│ └── Rbac/
│ ├── UiEnforcement.php (extend or add workspace variant)
│ └── ...
└── Services/
└── Auth/
├── RoleCapabilityMap.php (tenant-plane)
└── WorkspaceRoleCapabilityMap.php (new; workspace-plane)
database/migrations/
tests/Feature/
routes/web.php
bootstrap/app.php (middleware registration; Laravel 12)
bootstrap/providers.php (panel providers; Laravel 11+ rule)
Structure Decision: Laravel monolith. Implement Workspace scope via middleware + Filament pages/resources under the existing admin panel.
Phase 0 — Outline & Research (output: research.md)
Completed in research.md. Key outcomes:
- URL identity: slug preferred, id fallback.
- Current workspace persistence: session +
users.last_workspace_id. - Managed Tenant uniqueness: global unique
entra_tenant_id. - Zero memberships: neutral
/admin/no-accesspage. - Workspace-scoped routing + middleware enforcement.
- Global search scoped to active workspace.
- Workspace-level audit stream for membership mutations, implemented as
AuditLog-compatible entries (stable action IDs; redacted).
Phase 1 — Design & Contracts
Data model
See data-model.md for entities, constraints, and invariants.
Contracts
High-level entry-point contract is captured in contracts/admin-workspaces.openapi.yaml.
Quickstart
Manual verification guidance is in quickstart.md.
Post-design Constitution Re-check
Expected PASS: this feature is DB-only and authorization-heavy; ensure:
- deny-as-not-found for non-members is enforced at middleware + query levels,
- member-without-capability is 403,
- deterministic capability mapping tests exist,
- membership mutations are audited.
Phase 2 — Implementation Task Preview (for /speckit.tasks)
Planned task groupings:
- Database migrations + models (workspaces, memberships, user last_workspace_id, tenant workspace_id)
- Workspace context + middleware + routing (choose-workspace, no-access,
/admin/w/{workspace}prefix) - RBAC/capabilities (capability registry additions + role maps + policies)
- Filament resources/pages (Workspace CRUD, Members relation manager, managed tenant onboarding/list scoping)
- Global search scoping (workspace-safe global search queries)
- Auditing for membership mutations (AuditLog-compatible)
- Workspace lifecycle (archive/unarchive + selection invalidation)
- Deterministic capability mapping tests (golden/snapshot)
- Tests (Pest): last-owner guard, 404/403 semantics, global search scoping, workspace selection flows, migration verification
Complexity Tracking
Fill ONLY if Constitution Check has violations that must be justified
| Violation | Why Needed | Simpler Alternative Rejected Because |
|---|---|---|
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |