# Research: Workspace Model, Memberships & Managed Tenants (v2) **Date**: 2026-01-31 **Feature**: [spec.md](spec.md) This document consolidates the key design decisions needed to implement the spec in this repo. ## Decisions ### 1) Workspace identifier in URLs **Decision**: Prefer Workspace `slug` in URLs; fall back to numeric `id` when a workspace has no slug. **Rationale**: - Human-friendly URLs for admins (`/admin/w/acme/...`). - Keeps migration pain low because slugs can be introduced incrementally. **Alternatives considered**: - Always numeric id: easiest but less readable. - Slug-only required: clean but increases migration/validation burden. --- ### 2) Persisting current workspace selection **Decision**: Persist `current_workspace_id` in session AND store a nullable `users.last_workspace_id`. **Rationale**: - Session is canonical for the current request. - DB persistence improves UX across new sessions / devices without requiring the user to re-select every time. **Alternatives considered**: - Session only: simpler but annoying across logins. - URL-only: makes deep links harder and doesn’t support “default workspace” semantics. --- ### 3) Managed Tenant uniqueness **Decision**: Entra tenant id is globally unique (a Managed Tenant belongs to exactly one Workspace). **Rationale**: - Avoids ambiguous ownership and accidental double-management of the same Microsoft tenant. - Aligns with “no tenant-in-tenant” goal. **Alternatives considered**: - Unique per workspace: enables duplicates but creates confusing operational ownership. --- ### 4) Zero-membership entry behavior **Decision**: Show a neutral `/admin/no-access` page for users with 0 memberships (not 404). **Rationale**: - Clear UX while still not leaking any workspace existence. **Alternatives considered**: - 404: secure but confusing; users think the app is broken. --- ### 5) How to scope the admin panel without Filament tenancy **Decision**: Make Workspace context a first-class routing concern (`/admin/w/{workspace}/...`) enforced via middleware + session context. Do not use Filament tenancy (`/admin/t/{tenant}`) as the primary structure. **Rationale**: - Meets “no tenant-in-tenant” and removes the overloaded “tenant” concept in UI. - Middleware is the cleanest place to enforce deny-as-not-found for non-members. **Alternatives considered**: - Use Filament tenancy to represent workspaces: would keep the same tenancy mechanisms but continues the “tenant-in-tenant” confusion. --- ### 6) Global search scoping **Decision**: Global search must be scoped to the active Workspace and return no results outside it. **Rationale**: - Prevents leakage (no hints) and aligns with constitution RBAC-UX. - Repo already has a tenant-scoped global search trait; we can introduce a workspace-scoped variant. **Alternatives considered**: - Hide search results at render time: insufficient, because global search queries must also be scoped. --- ### 7) Audit logging for workspace membership changes **Decision**: Introduce a workspace-level audit log stream for membership mutations. **Rationale**: - Existing `audit_logs` table is tenant-scoped (requires `tenant_id`) and cannot represent workspace-only changes cleanly. - Additive schema avoids breaking existing auditing. **Alternatives considered**: - Make `audit_logs.tenant_id` nullable and add `workspace_id`: higher migration and code risk. ## Open Questions (implementation-level) These are not spec ambiguities but implementation choices to resolve while coding: - Whether to build a generic “scope context” abstraction (tenant/workspace) or implement a workspace-specific parallel to the existing tenant helpers. - How to progressively migrate existing Filament resources from `/admin/t/{tenant}` to `/admin/w/{workspace}` without breaking deep links (redirect strategy).