> ## Documentation Index
> Fetch the complete documentation index at: https://docs.8space.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Multi-Tenancy

> Tenant isolation, permission model, and workspace management

8Space implements a hierarchical multi-tenant architecture where organizations (tenants) contain projects, and users can belong to multiple tenants with different roles.

## Tenant Isolation Model

```mermaid theme={null}
graph TD
    A[User] -->|member of| B[Tenant: Acme Corp]
    A -->|member of| C[Tenant: Beta Inc]
    B -->|contains| D[Project: Marketing Site]
    B -->|contains| E[Project: Mobile App]
    C -->|contains| F[Project: Q1 Launch]
    D -->|has| G[Tasks]
    E -->|has| H[Tasks]
    F -->|has| I[Tasks]
    
    style A fill:#e1f5ff
    style B fill:#fff4e6
    style C fill:#fff4e6
    style D fill:#f3f4f6
    style E fill:#f3f4f6
    style F fill:#f3f4f6
```

### Key Principles

<CardGroup cols={2}>
  <Card title="Data Isolation" icon="shield">
    All project data is scoped to a tenant. Users cannot access projects from tenants they don't belong to.
  </Card>

  <Card title="Flexible Membership" icon="users">
    Users can be members of multiple tenants simultaneously with different roles in each.
  </Card>

  <Card title="URL-Based Routing" icon="link">
    Tenant slug in URL provides context: `/app/:tenantSlug/projects/:projectId`
  </Card>

  <Card title="Hierarchical Permissions" icon="key">
    Two-tier system: tenant-level roles and project-level roles
  </Card>
</CardGroup>

## Tenant Schema

### Tenant Table

```sql packages/app/supabase/migrations/20260217143000_multi_tenant_onboarding.sql theme={null}
create table public.tenants (
  id uuid primary key default gen_random_uuid(),
  name text not null check (char_length(name) between 1 and 120),
  slug text not null unique check (slug ~ '^[a-z0-9]+(?:-[a-z0-9]+)*$'),
  created_by uuid not null references public.profiles (id) on delete restrict,
  created_at timestamptz not null default now(),
  archived_at timestamptz
);

create index projects_tenant_idx on public.projects (tenant_id);
```

**Slug Requirements:**

* Lowercase alphanumeric with hyphens
* No leading/trailing hyphens
* Globally unique
* URL-safe for routing

### Tenant Membership

```sql theme={null}
create type public.tenant_role as enum ('owner', 'admin', 'member');

create table public.tenant_members (
  tenant_id uuid not null references public.tenants (id) on delete cascade,
  user_id uuid not null references public.profiles (id) on delete cascade,
  role public.tenant_role not null,
  joined_at timestamptz not null default now(),
  primary key (tenant_id, user_id)
);

create index tenant_members_user_idx on public.tenant_members (user_id);
```

## Role Hierarchy

### Tenant Roles

<Tabs>
  <Tab title="Owner">
    **Permissions:**

    * Full control over tenant
    * Manage tenant settings (name, slug)
    * Add/remove members
    * Delete tenant
    * Create projects

    **Database Check:**

    ```sql theme={null}
    select public.is_tenant_owner('tenant-uuid');
    ```

    **Typical Use Case:** Founder or organization admin
  </Tab>

  <Tab title="Admin">
    **Permissions:**

    * Manage tenant members (add/remove)
    * Create projects
    * Cannot delete tenant
    * Cannot modify tenant settings

    **Database Check:**

    ```sql theme={null}
    select public.current_tenant_role('tenant-uuid');
    -- Returns: 'admin'
    ```

    **Typical Use Case:** Team lead or department manager
  </Tab>

  <Tab title="Member">
    **Permissions:**

    * View tenant projects
    * Create projects (if tenant policy allows)
    * Cannot manage members

    **Database Check:**

    ```sql theme={null}
    select public.is_tenant_member('tenant-uuid');
    -- Returns: true
    ```

    **Typical Use Case:** Regular team member or contributor
  </Tab>
</Tabs>

### Project Roles

<Tabs>
  <Tab title="Owner">
    **Permissions:**

    * Full project control
    * Manage project members
    * Edit project settings
    * Delete project
    * Create/edit/delete tasks

    **Auto-Assignment:** User who creates the project
  </Tab>

  <Tab title="Editor">
    **Permissions:**

    * Create/edit/delete tasks
    * Manage task assignments
    * Add task dependencies
    * Cannot manage project members
    * Cannot delete project

    **Auto-Assignment:** Tenant admins when project is created
  </Tab>

  <Tab title="Viewer">
    **Permissions:**

    * Read-only access
    * View tasks and project data
    * Cannot modify anything

    **Auto-Assignment:** Tenant members when project is created
  </Tab>
</Tabs>

### Permission Matrix

| Action                 | Tenant Owner | Tenant Admin | Tenant Member | Project Owner | Project Editor | Project Viewer |
| ---------------------- | ------------ | ------------ | ------------- | ------------- | -------------- | -------------- |
| View tenant projects   | ✅            | ✅            | ✅             | ✅             | ✅              | ✅              |
| Create project         | ✅            | ✅            | ✅\*           | -             | -              | -              |
| Delete tenant          | ✅            | ❌            | ❌             | -             | -              | -              |
| Manage tenant members  | ✅            | ✅            | ❌             | -             | -              | -              |
| Edit project settings  | -            | -            | -             | ✅             | ❌              | ❌              |
| Manage project members | -            | -            | -             | ✅             | ❌              | ❌              |
| Create/edit tasks      | -            | -            | -             | ✅             | ✅              | ❌              |
| View tasks             | -            | -            | -             | ✅             | ✅              | ✅              |
| Delete project         | -            | -            | -             | ✅             | ❌              | ❌              |

<Info>
  \* Tenant members can create projects if the tenant policy allows. This is configurable per tenant.
</Info>

## Creating Tenants

### Database Function

```sql packages/app/supabase/migrations/20260217143000_multi_tenant_onboarding.sql theme={null}
create or replace function public.create_tenant_with_owner(
  p_name text,
  p_slug text default null
)
returns public.tenants
language plpgsql
security definer
set search_path = public
as $$
declare
  created public.tenants;
  base_slug text;
  candidate_slug text;
  suffix integer := 2;
begin
  if auth.uid() is null then
    raise exception 'Not authenticated';
  end if;

  if p_name is null or char_length(trim(p_name)) = 0 then
    raise exception 'Tenant name is required';
  end if;

  base_slug := public.normalize_tenant_slug(coalesce(nullif(trim(p_slug), ''), p_name));
  candidate_slug := base_slug;

  loop
    begin
      insert into public.tenants (name, slug, created_by)
      values (trim(p_name), candidate_slug, auth.uid())
      returning * into created;

      exit;
    exception
      when unique_violation then
        candidate_slug := base_slug || '-' || suffix::text;
        suffix := suffix + 1;
    end;
  end loop;

  insert into public.tenant_members (tenant_id, user_id, role)
  values (created.id, auth.uid(), 'owner')
  on conflict (tenant_id, user_id) do update
  set role = 'owner';

  return created;
end;
$$;
```

**Features:**

* Automatic slug generation from name
* Conflict resolution with numeric suffixes
* Creator automatically becomes owner
* Idempotent member insertion

### Slug Normalization

```sql theme={null}
create or replace function public.normalize_tenant_slug(p_value text)
returns text
language plpgsql
immutable
as $$
declare
  candidate text;
begin
  candidate := lower(coalesce(trim(p_value), ''));
  candidate := regexp_replace(candidate, '[^a-z0-9]+', '-', 'g');
  candidate := regexp_replace(candidate, '(^-|-$)', '', 'g');

  if candidate = '' then
    return 'space';
  end if;

  return candidate;
end;
$$;
```

**Examples:**

```typescript theme={null}
normalize_tenant_slug('Acme Corp')        // 'acme-corp'
normalize_tenant_slug('Beta-123!')        // 'beta-123'
normalize_tenant_slug('   Spaces   ')     // 'spaces'
normalize_tenant_slug('!')                // 'space' (fallback)
```

## Creating Projects in Tenants

### Updated Function Signature

```sql packages/app/supabase/migrations/20260217143000_multi_tenant_onboarding.sql theme={null}
create or replace function public.create_project_with_defaults(
  p_tenant_slug text,
  p_name text,
  p_description text
)
returns uuid
```

<Note>
  The function signature changed from accepting just `p_name` and `p_description` to requiring `p_tenant_slug` as the first parameter.
</Note>

### Auto-Member Addition

```sql theme={null}
insert into public.project_members (project_id, user_id, role)
select
  new_project_id,
  tm.user_id,
  case
    when tm.user_id = auth.uid() then 'owner'::public.project_role
    when tm.role in ('owner', 'admin') then 'editor'::public.project_role
    else 'viewer'::public.project_role
  end
from public.tenant_members tm
where tm.tenant_id = target_tenant_id
on conflict (project_id, user_id) do nothing;
```

**Role Mapping Logic:**

<Steps>
  <Step title="Project Creator">
    Always gets `owner` role regardless of tenant role
  </Step>

  <Step title="Tenant Owners/Admins">
    Automatically get `editor` role in new projects
  </Step>

  <Step title="Tenant Members">
    Automatically get `viewer` role in new projects
  </Step>

  <Step title="Future Members">
    When new users join the tenant, they are NOT automatically added to existing projects (must be invited)
  </Step>
</Steps>

## Permission Enforcement

### Database Functions

<CodeGroup>
  ```sql Tenant Permissions theme={null}
  create or replace function public.current_tenant_role(p_tenant_id uuid)
  returns public.tenant_role
  language sql
  stable
  security definer
  set search_path = public
  as $$
    select tm.role
    from public.tenant_members tm
    where tm.tenant_id = p_tenant_id
      and tm.user_id = auth.uid();
  $$;

  create or replace function public.is_tenant_member(p_tenant_id uuid)
  returns boolean
  as $$
    select public.current_tenant_role(p_tenant_id) is not null;
  $$;

  create or replace function public.is_tenant_owner(p_tenant_id uuid)
  returns boolean
  as $$
    select public.current_tenant_role(p_tenant_id) = 'owner';
  $$;
  ```

  ```sql Project Permissions theme={null}
  create or replace function public.current_project_role(p_project_id uuid)
  returns public.project_role
  as $$
    select pm.role
    from public.projects p
    join public.project_members pm
      on pm.project_id = p.id
     and pm.user_id = auth.uid()
    join public.tenant_members tm
      on tm.tenant_id = p.tenant_id
     and tm.user_id = auth.uid()
    where p.id = p_project_id;
  $$;

  create or replace function public.can_edit_project(p_project_id uuid)
  returns boolean
  as $$
    select public.current_project_role(p_project_id) in ('owner', 'editor');
  $$;
  ```
</CodeGroup>

<Warning>
  The `current_project_role` function requires BOTH project membership AND tenant membership. This prevents access if a user is removed from the tenant.
</Warning>

### Row Level Security

<Tabs>
  <Tab title="Tenant Policies">
    ```sql theme={null}
    -- Users can only view tenants they belong to
    create policy "tenants_select_members"
    on public.tenants for select to authenticated
    using (public.is_tenant_member(id));

    -- Only tenant owners can modify tenant
    create policy "tenants_update_owner"
    on public.tenants for update to authenticated
    using (public.is_tenant_owner(id))
    with check (public.is_tenant_owner(id));

    -- Only tenant owners can delete
    create policy "tenants_delete_owner"
    on public.tenants for delete to authenticated
    using (public.is_tenant_owner(id));

    -- Only tenant owners can manage members
    create policy "tenant_members_mutate_owner"
    on public.tenant_members for all to authenticated
    using (public.is_tenant_owner(tenant_id))
    with check (public.is_tenant_owner(tenant_id));
    ```
  </Tab>

  <Tab title="Project Policies">
    ```sql theme={null}
    -- Must be tenant member to create projects
    create policy "projects_insert_owner"
    on public.projects for insert to authenticated
    with check (
      created_by = auth.uid()
      and public.is_tenant_member(tenant_id)
    );

    -- Can only view projects you're a member of
    create policy "projects_select_members"
    on public.projects for select to authenticated
    using (public.is_project_member(id));

    -- Only project owners can modify
    create policy "projects_update_owner"
    on public.projects for update to authenticated
    using (public.is_project_owner(id))
    with check (public.is_project_owner(id));
    ```
  </Tab>

  <Tab title="Task Policies">
    ```sql theme={null}
    -- Members can view tasks
    create policy "tasks_select_members"
    on public.tasks for select to authenticated
    using (public.is_project_member(project_id));

    -- Editors can create tasks
    create policy "tasks_insert_editors"
    on public.tasks for insert to authenticated
    with check (public.can_edit_project(project_id));

    -- Editors can update tasks
    create policy "tasks_update_editors"
    on public.tasks for update to authenticated
    using (public.can_edit_project(project_id))
    with check (public.can_edit_project(project_id));

    -- Editors can delete tasks
    create policy "tasks_delete_editors"
    on public.tasks for delete to authenticated
    using (public.can_edit_project(project_id));
    ```
  </Tab>
</Tabs>

## Migration Strategy

### From Single-Tenant to Multi-Tenant

The migration `20260217143000_multi_tenant_onboarding.sql` automatically converts existing data:

<Steps>
  <Step title="Add tenant_id Column">
    ```sql theme={null}
    alter table public.projects
      add column tenant_id uuid references public.tenants (id) on delete cascade;
    ```

    Initially nullable to allow backfill
  </Step>

  <Step title="Generate Temporary Mapping">
    ```sql theme={null}
    create temporary table project_tenant_map (
      project_id uuid primary key,
      tenant_id uuid not null
    ) on commit drop;

    insert into project_tenant_map (project_id, tenant_id)
    select p.id, gen_random_uuid()
    from public.projects p;
    ```

    Each project gets a unique tenant ID
  </Step>

  <Step title="Create Tenants from Projects">
    ```sql theme={null}
    insert into public.tenants (id, name, slug, created_by, created_at)
    select
      map.tenant_id,
      case
        when char_length(trim(p.name)) = 0 then 'Workspace'
        else trim(p.name) || ' Space'
      end,
      'space-' || replace(p.id::text, '-', ''),
      p.created_by,
      p.created_at
    from public.projects p
    join project_tenant_map map on map.project_id = p.id;
    ```

    Tenant name derived from project name
  </Step>

  <Step title="Backfill tenant_id">
    ```sql theme={null}
    update public.projects p
    set tenant_id = map.tenant_id
    from project_tenant_map map
    where map.project_id = p.id;

    alter table public.projects
      alter column tenant_id set not null;
    ```

    Make column required after backfill
  </Step>

  <Step title="Migrate Memberships">
    ```sql theme={null}
    insert into public.tenant_members (tenant_id, user_id, role, joined_at)
    select distinct
      map.tenant_id,
      pm.user_id,
      case
        when pm.role = 'owner' then 'owner'::public.tenant_role
        when pm.role = 'editor' then 'admin'::public.tenant_role
        else 'member'::public.tenant_role
      end,
      pm.joined_at
    from public.project_members pm
    join project_tenant_map map on map.project_id = pm.project_id;
    ```

    Convert project roles to tenant roles
  </Step>

  <Step title="Drop Single-Tenant Triggers">
    ```sql theme={null}
    drop trigger if exists on_profile_created_join_projects on public.profiles;
    drop trigger if exists on_project_created_add_members on public.projects;
    ```

    Remove auto-join behavior from single-tenant mode
  </Step>
</Steps>

## URL Structure

### Routing Pattern

```
/app/:tenantSlug/projects/:projectId/tasks/:taskId
```

**Benefits:**

* Clear tenant context in URL
* Shareable links with implicit workspace
* SEO-friendly for public pages
* Simple authorization checks

### Repository Interface

```typescript packages/app/src/domain/repositories/interfaces.ts theme={null}
export interface TenantRepository {
  listTenants(userId: string): Promise<Tenant[]>;
  createTenantWithOwner(name: string, preferredSlug?: string): Promise<Tenant>;
}

export interface ProjectRepository {
  listProjects(userId: string, tenantSlug: string): Promise<Project[]>;
  createProjectWithDefaults(
    tenantSlug: string,
    input: CreateProjectInput
  ): Promise<Project>;
  // ...
}
```

**Key Points:**

* All project operations require `tenantSlug`
* Tenant context passed explicitly
* Type-safe tenant isolation

## Best Practices

<CardGroup cols={2}>
  <Card title="Always Validate Tenant Context" icon="shield-check">
    Never trust tenant slug from URL alone. Always verify user membership before data access.

    ```typescript theme={null}
    const tenant = await tenantRepo.getTenantBySlug(slug);
    if (!tenant || !tenant.isMember) {
      throw new Error('Access denied');
    }
    ```
  </Card>

  <Card title="Use Slug for Routing, ID for Data" icon="database">
    * URLs: Use human-readable slugs
    * Database: Use UUIDs for foreign keys
    * Convert slug to ID at API boundary
  </Card>

  <Card title="Enforce RLS at Database Level" icon="lock">
    Never rely on application-level checks alone. Database RLS provides defense-in-depth.
  </Card>

  <Card title="Handle Tenant Switching" icon="arrows-rotate">
    Users can be in multiple tenants. Clear client-side caches when switching contexts.
  </Card>
</CardGroup>

## Security Considerations

<AccordionGroup>
  <Accordion title="Tenant Data Leakage">
    **Risk:** User accesses data from wrong tenant via URL manipulation

    **Mitigation:**

    * RLS policies check tenant membership
    * `current_project_role` validates both tenant AND project membership
    * Database functions reject cross-tenant operations
  </Accordion>

  <Accordion title="Privilege Escalation">
    **Risk:** Member promotes themselves to owner

    **Mitigation:**

    * RLS policies restrict role changes to owners
    * `security definer` functions set `search_path = public`
    * All mutations require explicit permission checks
  </Accordion>

  <Accordion title="Tenant Deletion">
    **Risk:** Accidental tenant deletion loses all data

    **Mitigation:**

    * Soft delete via `archived_at` timestamp
    * Only owners can archive tenants
    * Cascade deletes handled by foreign keys
    * Consider implementing trash/recovery period
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Architecture Overview" icon="diagram-project" href="/architecture/overview">
    Return to high-level architecture concepts
  </Card>

  <Card title="Database Schema" icon="database" href="/architecture/database-schema">
    Explore complete table definitions and relationships
  </Card>
</CardGroup>
