TenantAtlas/specs/068-workspaces-v2/research.md
2026-02-01 12:19:57 +01:00

3.8 KiB
Raw Blame History

Research: Workspace Model, Memberships & Managed Tenants (v2)

Date: 2026-01-31
Feature: 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 doesnt 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).