# Multi-Workspace Architecture for Team Data Isolation in Multica

> Discover Multica's multi-workspace architecture for robust team data isolation. Learn how its middleware ensures secure data access by automatically filtering queries by workspace ID.

- Repository: [multica-ai/multica](https://github.com/multica-ai/multica)
- Tags: architecture
- Published: 2026-04-11

---

**Multica implements strict team data isolation by forcing every API request through a workspace-scoped middleware layer that validates membership and injects the workspace ID into the request context, ensuring all database queries automatically filter by the foreign key `workspace_id`.**

The multica-ai/multica repository is built around a foundational security principle: every piece of team data lives inside a dedicated workspace silo. This architecture ensures that issues, members, repositories, and settings are never accidentally exposed across team boundaries.

## Core Components of the Multi-Workspace Architecture

### Workspace and Member Models

At the database layer, the **workspace** serves as the logical container for all team data. According to the source code in [`server/pkg/db/generated/workspace.sql.go`](https://github.com/multica-ai/multica/blob/main/server/pkg/db/generated/workspace.sql.go), every workspace is defined by a unique ID, name, slug, and description. Critically, all resources reference this table via a foreign key.

The **member** table—defined in [`server/pkg/db/generated/member.sql.go`](https://github.com/multica-ai/multica/blob/main/server/pkg/db/generated/member.sql.go)—creates the bridge between users and workspaces. Each row links a `user_id` to a `workspace_id` and carries a role (`owner`, `admin`, or `member`). The middleware relies on `GetMemberByUserAndWorkspace` to verify that the caller has access before any data operation proceeds.

### Request-Level Middleware

The enforcement mechanism resides in [`server/internal/middleware/workspace.go`](https://github.com/multica-ai/multica/blob/main/server/internal/middleware/workspace.go). The `RequireWorkspaceMember` function constructs a chain that:

1. Extracts a workspace identifier from either the query parameter `?workspace_id=` or the HTTP header `X-Workspace-ID`
2. Looks up the caller via `X-User-ID`
3. Validates membership using `queries.GetMemberByUserAndWorkspace`
4. Injects both the `workspaceID` and `member` object into the request context using `SetMemberContext`

```go
// server/internal/middleware/workspace.go
func RequireWorkspaceMember(queries *db.Queries) func(http.Handler) http.Handler {
    return buildMiddleware(queries, resolveWorkspaceID, nil)
}

```

### Context-Aware Handlers

Downstream handlers retrieve the validated context data through helper functions. These functions extract the workspace-scoped values without requiring handlers to re-implement authentication logic:

```go
// server/internal/middleware/workspace.go
func WorkspaceIDFromContext(ctx context.Context) string {
    id, _ := ctx.Value(ctxKeyWorkspaceID).(string)
    return id
}

func MemberFromContext(ctx context.Context) (db.Member, bool) {
    m, ok := ctx.Value(ctxKeyMember).(db.Member)
    return m, ok
}

```

## How Data Isolation Works in Practice

The multi-workspace architecture enforces isolation through a consistent four-stage pipeline for every request.

**1. Client supplies workspace identifier**
The API client must include the target workspace in either the query string or the `X-Workspace-ID` header.

**2. Middleware validates membership**
The `RequireWorkspaceMember` middleware resolves the ID, queries the `member` table to confirm the user belongs to that workspace, and aborts the request if the relationship does not exist.

**3. Handler retrieves context values**
Any handler can safely call `WorkspaceIDFromContext` or `MemberFromContext` to receive the pre-validated identifiers. Because the middleware has already confirmed membership, handlers trust these values implicitly.

**4. Database enforces foreign-key constraints**
All generated SQL queries accept a `workspaceID` parameter that filters the result set. For example, the workspace list query joins through the member table to ensure users only see workspaces they belong to:

```sql
SELECT w.id, w.name, …
FROM workspace w
JOIN member m ON m.workspace_id = w.id
WHERE m.user_id = $1
ORDER BY w.created_at ASC

```

## Critical Source Files and Their Roles

- [`server/internal/middleware/workspace.go`](https://github.com/multica-ai/multica/blob/main/server/internal/middleware/workspace.go) – Implements `RequireWorkspaceMember`, context injection, and the helper functions `WorkspaceIDFromContext` and `MemberFromContext`.
- [`server/pkg/db/generated/member.sql.go`](https://github.com/multica-ai/multica/blob/main/server/pkg/db/generated/member.sql.go) – Provides the `GetMemberByUserAndWorkspace` lookup that authorizes every request.
- [`server/pkg/db/generated/workspace.sql.go`](https://github.com/multica-ai/multica/blob/main/server/pkg/db/generated/workspace.sql.go) – Contains auto-generated CRUD methods that accept `workspace_id` as a mandatory filtering argument.
- [`server/internal/handler/workspace.go`](https://github.com/multica-ai/multica/blob/main/server/internal/handler/workspace.go) – Demonstrates how handlers create and manage workspaces while respecting the isolation boundaries established by the middleware.

## Implementation Examples

### Creating a New Workspace

When a user creates a workspace, they automatically become the first member with the `owner` role. The handler at lines 32-70 of [`server/internal/handler/workspace.go`](https://github.com/multica-ai/multica/blob/main/server/internal/handler/workspace.go) inserts the workspace row, then immediately creates the membership record at lines 80-84.

```bash
curl -X POST https://api.multica.dev/v1/workspaces \
  -H "Authorization: Bearer <jwt>" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "Acme Team",
        "slug": "acme",
        "description": "Acme Corp workspace"
      }'

```

### Adding Members with Validation

This request demonstrates how the middleware intercepts traffic to validate workspace access before the handler executes:

```bash
curl -X POST https://api.multica.dev/v1/workspaces/acme/members \
  -H "Authorization: Bearer <jwt>" \
  -H "X-Workspace-ID: <workspace‑uuid>" \
  -H "Content-Type: application/json" \
  -d '{"email":"bob@example.com","role":"member"}'

```

The `CreateMember` handler (lines 52-84) receives the request only after `RequireWorkspaceMember` has confirmed the caller belongs to the specified workspace.

### Querying Workspace-Scoped Data

Handlers ignore user-supplied workspace identifiers in request bodies, instead using the context value injected by the middleware:

```go
func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) {
    wsID := middleware.WorkspaceIDFromContext(r.Context())
    issues, err := h.Queries.ListIssues(r.Context(), db.ListIssuesParams{
        WorkspaceID: parseUUID(wsID),
    })
    // … return JSON …
}

```

### Enforcing Role-Based Access Control

After membership validation, handlers check the member's role for sensitive operations. Only `owner` roles can delete workspaces:

```go
func (h *Handler) DeleteWorkspace(w http.ResponseWriter, r *http.Request) {
    member, ok := middleware.MemberFromContext(r.Context())
    if !ok || member.Role != "owner" {
        writeError(w, http.StatusForbidden, "only owners may delete a workspace")
        return
    }
    // safe to delete because the workspace belongs to this member
}

```

## Summary

Multica's multi-workspace architecture guarantees data isolation through four integrated layers:

- **Explicit workspace identifiers** required in every API request via headers or query parameters
- **Middleware validation** that lookups up the `member` record and rejects unauthorized requests before they reach handlers
- **Context injection** providing handlers with pre-verified `workspaceID` and `member` objects
- **Database-level filtering** where all queries use the `workspace_id` foreign key to scope results

## Frequently Asked Questions

### How does Multica prevent cross-workspace data leakage?

The `RequireWorkspaceMember` middleware in [`server/internal/middleware/workspace.go`](https://github.com/multica-ai/multica/blob/main/server/internal/middleware/workspace.go) validates that the caller's user ID exists in the `member` table for the requested workspace ID. Only after this check does the request proceed to handlers, which then use the validated `workspaceID` from context to query the database. Even if a malicious actor crafts a request with a spoofed workspace identifier, they cannot bypass the membership lookup.

### What happens if a request lacks a workspace identifier?

If a request reaches the `RequireWorkspaceMember` middleware without an `X-Workspace-ID` header or `?workspace_id` query parameter, the `resolveWorkspaceID` function fails to extract an identifier. The middleware chain rejects the request before it reaches any handler, returning an error that forces the client to specify a workspace.

### Can a user belong to multiple workspaces with different roles?

Yes. The `member` table schema supports many-to-many relationships between users and workspaces. A single user can have an `owner` role in one workspace and a `member` role in another. Each request is evaluated independently based on the workspace ID provided in that specific call, and the middleware loads the appropriate role for that context.

### Where is the workspace membership validated in the request lifecycle?

Validation occurs in the middleware layer before the HTTP handler executes. Specifically, the `buildMiddleware` function in [`server/internal/middleware/workspace.go`](https://github.com/multica-ai/multica/blob/main/server/internal/middleware/workspace.go) calls `queries.GetMemberByUserAndWorkspace` to load the membership record. If this query returns no rows, the middleware aborts the request with an authorization error, ensuring handlers never process requests from non-members.