Files
goclaw/docs/23-multi-tenant-architecture.md
T
Duy /zuey/ 532ff91d8e fix(security): harden upstream critical surfaces (#32)
* fix(security): harden upstream critical surfaces

Refs #30

* fix(security): close pre-landing review gaps

Refs #30

* fix(security): close official release blockers
2026-05-20 16:33:49 +07:00

15 KiB

Multi-Tenant Integration Guide

GoClaw is an AI agent gateway — it handles agents, chat, sessions, tools, MCP servers, and memory. It supports two deployment modes:

  1. Personal / Single-tenant — Use GoClaw directly as your AI backend. Built-in dashboard included.
  2. SaaS / Multi-tenant — Integrate GoClaw behind your application. API keys bridge the two systems.

Deployment Modes

Mode 1: Personal Use (Single-Tenant)

Use GoClaw as a standalone AI backend with its built-in web dashboard. No separate frontend or backend needed.

graph LR
    U[You] -->|browser| GC[GoClaw Dashboard<br/>+ Gateway]
    GC --> AG[Agents / Chat / Tools]
    AG --> DB[(PostgreSQL)]
    AG -->|LLM calls| LLM[Anthropic / OpenAI / Gemini / ...]

How it works:

  • Log in with the gateway token via the built-in web dashboard
  • Create agents, configure LLM providers, chat — all from the dashboard
  • Connect chat channels (Telegram, Discord, etc.) for messaging
  • All data lives under the default "master" tenant — no tenant config needed

Setup:

# 1. Build and onboard
go build -o goclaw . && ./goclaw onboard

# 2. Start the gateway
source .env.local && ./goclaw

# 3. Open dashboard at http://localhost:3777
#    Log in with your gateway token + user ID "system"

When to use: Personal AI assistant, small team, self-hosted AI tools, development/testing.

Scaling up: When you need multiple isolated environments (clients, departments, projects), create additional tenants. Multi-tenant features activate automatically — no migration needed.


Mode 2: SaaS Integration (Multi-Tenant)

Integrate GoClaw as the AI engine behind your SaaS application. Your app handles auth, billing, and UI. GoClaw handles AI. Each tenant is fully isolated — agents, sessions, memory, teams, providers, and files.

graph TB
    subgraph "Tenant A"
        FEa[Frontend A]
        BEa[Backend A]
        TGa[Telegram Bot A]
    end

    subgraph "Tenant B"
        FEb[Frontend B]
        BEb[Backend B]
        DCb[Discord Bot B]
    end

    subgraph "GoClaw Gateway"
        subgraph "Entry Points"
            HTTP[HTTP API]
            WS[WebSocket]
            CH[Channel Manager]
        end

        TI{Tenant Isolation<br/>Layer}

        subgraph "Tenant-Scoped Engine"
            AG[Agent Loop]
            TOOLS[Tools / Skills / MCP]
            MEM[Memory / KG]
        end

        DB[(PostgreSQL<br/>WHERE tenant_id = $N)]
    end

    FEa -->|authenticated| BEa
    BEa -->|API Key A + user_id| HTTP
    TGa -->|webhook| CH

    FEb -->|authenticated| BEb
    BEb -->|API Key B + user_id| HTTP
    DCb -->|webhook| CH

    HTTP --> TI
    WS --> TI
    CH --> TI

    TI -->|ctx with tenant_id| AG
    AG --> TOOLS
    AG --> MEM
    AG --> DB
    TOOLS --> DB
    MEM --> DB

How it works:

  • Each tenant's backend connects via a tenant-bound API key — GoClaw auto-scopes all data
  • Chat channels (Telegram, Discord, etc.) connect directly — tenant resolved from channel instance config
  • The Tenant Isolation Layer resolves tenant_id from credentials and injects it into Go context
  • Every SQL query enforces WHERE tenant_id = $N — fail-closed, no cross-tenant leakage

When to use: SaaS products with AI features, multi-client platforms, white-label AI solutions.

Connection Types Summary

All connections go through the Tenant Isolation Layer before reaching the agent engine:

Connection Auth Method Tenant Resolution Isolation
HTTP API Bearer token (API key or gateway token) Auto from API key's tenant_id Per-request
WebSocket Token on connect (API key or gateway token) Auto from API key's tenant_id Per-session
Chat Channels None (direct webhook/WS) Baked into channel instance DB config Per-instance
Dashboard Gateway token or browser pairing User's tenant membership Per-session

Tenant Isolation Layer — resolves credentials → injects tenant_id into Go context.Context → all downstream SQL queries enforce WHERE tenant_id = $N. Fail-closed: missing tenant = error, never unfiltered data.


Tenant Setup (Multi-Tenant Only)

sequenceDiagram
    participant Admin as System Admin
    participant GC as GoClaw API

    Admin->>GC: tenants.create {name: "Acme Corp", slug: "acme"}
    GC-->>Admin: {id: "tenant-uuid", slug: "acme"}

    Admin->>GC: tenants.users.add {tenant_id, user_id: "user-123", role: "admin"}

    Admin->>GC: api_keys.create {tenant_id, scopes: ["operator.read", "operator.write"]}
    GC-->>Admin: {key: "goclaw_sk_abc123..."}

    Note over Admin: Store API key in your backend's config/secrets

Each tenant gets isolated: agents, sessions, teams, memory, LLM providers, MCP servers, skills. A tenant-bound API key automatically scopes every request — no extra headers needed.


Tenant Resolution

GoClaw determines the tenant from the credentials used to connect:

Credential Tenant Resolution Use Case
Gateway token + owner user ID All tenants (cross-tenant) System administration
Gateway token + non-owner user ID Master tenant by default, or a membership-validated X-GoClaw-Tenant-Id / tenant_id hint Dashboard users
API key (tenant-bound) Auto from key's tenant_id Normal SaaS integration
API key (system-level) + X-GoClaw-Tenant-Id Header value (UUID or slug), while keeping the key's original role Cross-tenant tools
Browser pairing Master tenant by default, or a membership-validated tenant hint Dashboard operators
No credentials Master tenant Loopback local development or explicit GOCLAW_ALLOW_INSECURE_NO_AUTH=1 only

Owner IDs: Configured via GOCLAW_OWNER_IDS env var (comma-separated). Only owners get cross-tenant access with the gateway token. Default: system.

Recommended for SaaS: Use tenant-bound API keys. The tenant is resolved automatically from the key — your backend doesn't need to send any tenant header.


HTTP API

All HTTP endpoints accept standard headers:

Header Required Description
Authorization Yes Bearer <api-key-or-gateway-token>
X-GoClaw-User-Id Yes Your app's user ID (max 255 chars). Scopes sessions and per-user data
X-GoClaw-Tenant-Id No Tenant UUID or slug. Only needed for system-level keys
X-GoClaw-Agent-Id No Target agent ID (alternative to model field)
Accept-Language No Locale for error messages: en, vi, zh

Chat (OpenAI-Compatible)

curl -X POST https://goclaw.example.com/v1/chat/completions \
  -H "Authorization: Bearer goclaw_sk_abc123..." \
  -H "X-GoClaw-User-Id: user-456" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "agent:my-agent",
    "messages": [{"role": "user", "content": "Hello"}]
  }'

The model field uses agent:<agent-key> format. The API key is bound to tenant "Acme Corp" — the response only includes data from that tenant.

List Resources

# List agents
curl https://goclaw.example.com/v1/agents \
  -H "Authorization: Bearer goclaw_sk_abc123..." \
  -H "X-GoClaw-User-Id: user-456"

# List sessions
curl https://goclaw.example.com/v1/sessions \
  -H "Authorization: Bearer goclaw_sk_abc123..." \
  -H "X-GoClaw-User-Id: user-456"

System Admin (Cross-Tenant)

# List agents for a specific tenant (requires gateway token + owner user ID)
curl https://goclaw.example.com/v1/agents \
  -H "Authorization: Bearer $GATEWAY_TOKEN" \
  -H "X-GoClaw-Tenant-Id: acme" \
  -H "X-GoClaw-User-Id: system"

WebSocket Integration

For real-time features (streaming chat, live events), connect via WebSocket:

sequenceDiagram
    participant FE as Your Frontend
    participant BE as Your Backend
    participant GC as GoClaw Gateway

    FE->>BE: User sends message (authenticated)
    BE->>GC: WS connect {token: "goclaw_sk_abc...", user_id: "user-456"}
    GC-->>BE: {role: "operator", tenant_id, tenant_name, tenant_slug}

    BE->>GC: chat.send {agent_key: "my-agent", message: "Hello"}
    GC-->>BE: event: agent {type: "chunk", content: "Hi..."}
    BE-->>FE: Stream response to user
    GC-->>BE: event: agent {type: "run.completed"}

After connect, all methods are auto-scoped to the API key's tenant. Events are server-side filtered — your backend only receives events belonging to its tenant.

Protocol: Frame types req (client→server), res (server→client), event (async push). Protocol version 3.


Chat Channels

Chat channels (Telegram, Discord, Zalo, Slack, WhatsApp, Feishu) connect directly to GoClaw — no API key needed. Tenant isolation is baked into the channel instance at registration time.

sequenceDiagram
    participant TG as Telegram
    participant CH as Channel Manager
    participant TI as Tenant Isolation
    participant AG as Agent Loop
    participant DB as PostgreSQL

    Note over CH: Channel instance loaded from DB<br/>with tenant_id baked in

    TG->>CH: Incoming message (webhook)
    CH->>TI: Resolve tenant from instance config
    TI->>AG: ctx with tenant_id + user_id
    AG->>DB: Query with WHERE tenant_id = $N
    AG-->>CH: Agent response
    CH-->>TG: Reply to user

Each channel instance stores its tenant_id in the channel_instances table. When a message arrives, the Channel Manager looks up the instance config and injects the tenant context — no headers or tokens required.

Channel Protocol Tenant Source
Telegram Webhook Instance config
Discord WebSocket Instance config
Zalo Webhook Instance config
Slack Events API Instance config
WhatsApp Webhook Instance config
Feishu/Lark Webhook Instance config

API Key Scopes

API keys use scopes to control access level:

Scope Role Permissions
operator.admin admin Full access — agents, config, API keys, tenants
operator.read viewer Read-only — list agents, sessions, configs
operator.write operator Read + write — chat, create sessions, manage agents
operator.approvals operator Approve/reject execution requests
operator.provision operator Create tenants + manage tenant users
operator.pairing operator Manage device pairing

A key with ["operator.read", "operator.write"] gets operator role. A key with ["operator.admin"] gets admin role.


Per-Tenant Overrides

Tenants can customize their environment without affecting other tenants:

Feature Scope How
LLM Providers Per-tenant provider configs Each tenant registers own API keys + models
Builtin Tools Enable/disable + settings override per tenant builtin_tool_tenant_configs (enabled + settings JSONB). 4-tier overlay (per-agent > tenant > global > hardcoded) resolved at Execute time — see docs/03-tools-system.md § 14
Skills Enable/disable per tenant skill_tenant_configs table
MCP Servers Per-tenant + per-user credentials Server-level shared, user-level overrides
MCP Require User Credentials Per-server setting settings.require_user_credentials — forces per-user API keys

MCP servers support two credential tiers:

  • Server-level (shared): configured in the MCP server form, used by all users
  • User-level (overrides): configured via "My Credentials", per-user API keys merged at runtime (user wins on key collision)

When require_user_credentials is enabled, users without personal credentials cannot use that MCP server.


Security

Concern How GoClaw Handles It
API key exposure Keys stay in your backend — never sent to browser
Cross-tenant data access All SQL queries include WHERE tenant_id = $N (fail-closed)
Event leakage Server-side 3-mode filter: unscoped admin, scoped admin, regular user
Missing tenant context Fail-closed: returns error, never unfiltered data
API key storage Keys hashed with SHA-256 at rest; only prefix shown in UI
Tenant impersonation Tenant resolved from API key binding, not client headers
Cross-tenant privilege escalation on global writes store.IsMasterScope(ctx) + http.requireMasterScope(w, r) guard every admin-gated write to global tables (builtin_tools, package management, config.*). Symmetric requireTenantAdmin guards tenant-scoped writes. Predicate shared between HTTP + WS layers. See commits b419f352 (Phase 1 WS) + 6d7473b5 (Phase 0b HTTP)
Privilege escalation Role derived from key scopes, not client claims
Gateway token abuse Only configured owner IDs get cross-tenant; others are tenant-scoped
System config access Config page restricted to cross-tenant owners only
Logout isolation Tenant scope cleared from localStorage on logout
Tenant access revocation Proactive WS event + TENANT_ACCESS_REVOKED error forces immediate UI logout
File URL security HMAC-signed file tokens (?ft=) — no gateway token in URLs

Tenant Data Model

erDiagram
    TENANTS ||--o{ TENANT_USERS : "members"
    TENANTS ||--o{ API_KEYS : "keys"
    TENANTS ||--o{ AGENTS : "owns"
    TENANTS ||--o{ SESSIONS : "owns"
    TENANTS ||--o{ TEAMS : "owns"
    TENANTS ||--o{ LLM_PROVIDERS : "configures"
    TENANTS ||--o{ MCP_SERVERS : "registers"
    TENANTS ||--o{ SKILLS : "manages"

    TENANTS {
        uuid id PK
        string name
        string slug UK
        string status
        jsonb settings
    }

    TENANT_USERS {
        uuid tenant_id FK
        string user_id
        string role
    }

    API_KEYS {
        uuid id PK
        uuid tenant_id FK "NULL = system key"
        string owner_id
        text[] scopes
        boolean revoked
    }

40+ tables carry tenant_id with NOT NULL constraint. Exception: api_keys.tenant_id is nullable — NULL means system-level cross-tenant key.

v3 Tenant-Scoped Stores

New v3 stores (evolution, vault, episodic, agent_links) all enforce tenant isolation:

Store Purpose Tenant Scoping
EvolutionMetrics Track agent improvement suggestions WHERE tenant_id = $N
EvolutionSuggestions Store LLM-generated optimizations WHERE tenant_id = $N
Vault Persistent data storage for agents WHERE tenant_id = $N
Episodic Episodic memory for agents WHERE tenant_id = $N
AgentLink Delegation links between agents WHERE tenant_id = $N

All v3 stores follow the same tenant isolation pattern — all queries include WHERE tenant_id = $N at the SQL level.

Master tenant (UUID 0193a5b0-7000-7000-8000-000000000001): All legacy/default data. Single-tenant deployments use this exclusively.


Environment Variables

Variable Default Description
GOCLAW_OWNER_IDS system Comma-separated user IDs with cross-tenant access
GOCLAW_LOG_LEVEL info Log level: debug, info, warn, error
GOCLAW_CONFIG config.json5 Path to gateway config file