Multi-Workspace Architecture for Team Data Isolation in Multica

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, 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—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. 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
// 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:

// 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:

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

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 inserts the workspace row, then immediately creates the membership record at lines 80-84.

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:

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:

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:

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 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 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →