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

# Multi-Tenancy & Workspace Isolation

> How Switchbord enforces strict data isolation between tenant organizations, preventing cross-workspace data access at every layer.

## Isolation Model

Switchbord is a multi-tenant platform where each tenant organization operates as an independent **workspace**. The isolation guarantee is:

> A user in workspace A cannot read, write, or act on data belonging to workspace B — regardless of how API calls are constructed.

This guarantee is enforced server-side at the API layer, not through client-side routing or UI controls alone.

## Workspace Context Resolution

### `resolveCallerWorkspaceFromSupabase(userId)`

Every authenticated API route resolves workspace context by querying the `workspace_members` table using the authenticated user's ID:

```typescript theme={null}
// Server-side only — never reads env vars or client headers
async function resolveCallerWorkspaceFromSupabase(userId: string) {
  const { data, error } = await supabase
    .from('workspace_members')
    .select('workspace_id, role')
    .eq('user_id', userId)
    .single();

  if (error || !data) throw new UnauthorizedError();
  return { workspaceId: data.workspace_id, role: data.role };
}
```

Key properties:

* Reads from `workspace_members`, not environment variables
* Returns the workspace the user actually belongs to — cannot be overridden by request parameters
* Throws immediately if the user is not a member of any workspace
* Used as the foundation for all workspace-scoped data access

### `requireSettingsAccess()`

All 13 settings API routes use `requireSettingsAccess()` as their first operation, which calls `resolveCallerWorkspaceFromSupabase` and enforces a minimum role level before any settings data is read or written:

```typescript theme={null}
const { workspaceId, role } = await requireSettingsAccess(req, ['owner', 'admin']);
// workspaceId is now trusted — use it for all subsequent queries
```

<Warning>
  Never pass `workspaceId` as a query parameter or request body field that is used for authorization. Always derive it server-side from the authenticated session.
</Warning>

## Deprecated Pattern: `readSingleWorkspace()`

The function `readSingleWorkspace()` read workspace configuration from environment variables, which is incompatible with multi-tenancy. It has been **deprecated and removed** from all production code paths.

Migration path for any code still referencing it:

```typescript theme={null}
// DEPRECATED — do not use
const workspace = await readSingleWorkspace();

// CORRECT — resolve from the authenticated user
const { workspaceId } = await resolveCallerWorkspaceFromSupabase(userId);
const workspace = await getWorkspaceById(workspaceId);
```

## Settings Route Threading

All 13 settings API routes thread `workspaceId` derived from `requireSettingsAccess()` through every downstream query. No settings route reads workspace data without first establishing the caller's workspace context.

Example pattern for a settings route:

```typescript theme={null}
export async function GET(req: NextRequest) {
  const { workspaceId, role } = await requireSettingsAccess(req, ['owner', 'admin', 'developer']);

  const settings = await db
    .select()
    .from(workspaceSettings)
    .where(eq(workspaceSettings.workspaceId, workspaceId))  // always scoped
    .limit(1);

  return Response.json(settings);
}
```

## Message Dispatch Isolation

The `dispatchMessageViaMetaInSupabase` function reads Meta credentials from the workspace identified by `job.workspace_id` — the workspace that owns the job, not a global configuration:

```typescript theme={null}
async function dispatchMessageViaMetaInSupabase(job: MessageJob) {
  // Credentials fetched from the workspace that owns this job
  const credentials = await getWorkspaceMetaCredentials(job.workspace_id);
  // workspace A's jobs always use workspace A's Meta token
  return sendViaMetaAPI(credentials, job.payload);
}
```

This means:

* Workspace A cannot cause messages to be sent via Workspace B's Meta phone number
* Credential leakage between tenants at the dispatch layer is structurally impossible
* Each workspace configures and rotates its own Meta access token independently

## Invite-Only Join Model

Users can only join a workspace through an explicit invitation from an existing workspace admin or owner. There is no self-serve workspace discovery or join flow. This ensures:

* Workspace membership is always intentional
* New members receive only the role explicitly granted by the inviting admin
* Removing a member immediately revokes all access, including active sessions

See [Authentication & Access Control](/security/authentication) for session revocation details.

## API Key Scoping

API keys are scoped to the workspace that created them. All API key operations (list, delete, revoke) filter by the caller's resolved `workspace_id`:

```typescript theme={null}
// API key deletion — always scoped to caller's workspace
await db.delete(apiKeys)
  .where(
    and(
      eq(apiKeys.id, keyId),
      eq(apiKeys.workspaceId, workspaceId)  // cannot delete keys from other workspaces
    )
  );
```

## AI Assistant Isolation

The `/api/ai-assistant` endpoint previously lacked an auth guard. It now requires authentication and resolves workspace context before processing any request. AI context is scoped to contacts and conversations within the caller's workspace only.

## Compliance Mapping

| Standard  | Control                                           | Implementation                                                         |
| --------- | ------------------------------------------------- | ---------------------------------------------------------------------- |
| ISO 27001 | A.9.1 — Access control policy                     | Workspace-scoped access enforced at API layer                          |
| ISO 27001 | A.9.4 — System and application access control     | `requireSettingsAccess()` on all settings routes                       |
| GDPR      | Art.25 — Data protection by design and by default | Workspace isolation by default; no cross-tenant data leakage by design |
| GDPR      | Art.32 — Security of processing                   | Technical measures preventing unauthorized cross-workspace access      |
| SOC 2     | CC6.1 — Logical and physical access controls      | Workspace membership enforced server-side                              |
| SOC 2     | CC6.2 — Prior to issuing system credentials       | Invite-only model; credentials issued only on explicit admin action    |

<Tip>
  For the full compliance control mapping, see the [Compliance Matrix](/security/compliance-matrix).
</Tip>
