All Articles OpenClaw

OpenClaw Multi-User Patterns: Workspace Separation and Trust Boundaries

Look, I'm gonna be direct with you: OpenClaw is built for single-user workflows. That's not a limitation—it's a feature.

Look, I’m gonna be direct with you: OpenClaw is built for single-user workflows. That’s not a limitation—it’s a feature. But I know what you’re thinking: “I want to deploy this for my team, my organization, my SaaS product.” And you absolutely can. You just need to understand what you’re walking into first.

The problem isn’t technical complexity alone. It’s trust. It’s permission boundaries. It’s the difference between “isolated workspaces” and “actually isolated workspaces.” Let’s dig into how to build those boundaries properly, because half-measures here create security theater, not security.

Why Single-User Isn’t a Limitation

Before we talk about multi-user patterns, we need to understand why OpenClaw started here. And honestly, this matters for your architecture decisions.

OpenClaw’s core design treats the AI agent as a personal tool—like having a skilled assistant who knows your context, your preferences, your data, your priorities. That agent lives in your session, reads your memory, accesses your tools, and operates under your intention. When you design for this, you can make aggressive performance optimizations. You don’t need distributed locking. You don’t need audit logs for cross-user conflicts. You don’t need to resolve permission collisions because there’s only one user.

Think about what that means operationally. When the agent executes a tool, there’s no question about “whose credentials does this use?” or “does User B have access to this resource?” You already know: it’s the user’s resource. The agent operates with their context. Every decision it makes is scoped to their workspace. This simplicity cascades through the entire architecture. Memory storage doesn’t need user ID columns—there’s only one user’s memory in that instance. Backup procedures don’t need to worry about cross-user data leakage. Audit logs don’t need to distinguish between users.

Single-user also means something critical for privacy: memory isolation is simple. User A’s conversation history, learned preferences, and accumulated context don’t leak into User B’s session. No cross-contamination of training data. No “Oops, we served the wrong user’s memory vector to another request.” Just clean, orthogonal sessions. This is why OpenClaw doesn’t have the “User B somehow knows User A’s preferences” problem that plagued multi-user AI systems before.

But here’s where people get tripped up: they see OpenClaw’s capabilities and think “This would be perfect for team collaboration.” And it would be—if you architect it right. The moment you try to make one OpenClaw instance serve multiple users concurrently, you’ve fundamentally changed the system. That’s not OpenClaw anymore. That’s “a multi-user system built on top of OpenClaw components.”

Here’s the subtle distinction: you’re not “making OpenClaw multi-user.” OpenClaw itself stays single-user. What you’re doing is layering a multi-user control plane on top of it. Think of it like running multiple single-player games on the same computer—each game is isolated, but you’re managing multiple instances.

So let’s talk about how to do that without creating security holes.

The Core Challenge: Permission Boundaries vs. Shared Infrastructure

Here’s the scenario: you want to deploy OpenClaw for your team of five people. Each person gets their own AI agent to automate workflows, research competitors, manage calendars, handle email triage. They shouldn’t see each other’s data. They shouldn’t access each other’s tools. They shouldn’t read each other’s memory.

Sounds straightforward, right? Separate database records, separate file storage, separate session tokens. Done.

Except it’s not done.

Because OpenClaw uses agents and agents can be shared. An agent is a configured AI worker—let’s say “research-agent” that’s trained to audit websites and compile competitor intel. You could configure this agent once and have all five team members use it. That’s efficient. But who controls what that agent does? What data does it access? What tools does it run?

Here’s where the boundary gets fuzzy.

If the research-agent has access to a “web scraper” tool, and that tool has credentials to authenticate with a company’s API, then any user of that agent has implicit access to the API. You can’t grant the tool to User A and not User B if they both use the shared agent. Unless you add a permission layer inside the agent itself—but now you’ve got distributed permission checks. Now you’ve got state that needs to be consistent across invocations. Now you’ve got complexity.

This is why explicit workspace separation matters.

Pattern 1: Per-User Workspace Isolation

The cleanest approach is to treat each user as a separate workspace. A workspace is a bounded environment with:

  • Separate data store (isolated database schema, isolated file storage)
  • Dedicated agent instances (not shared)
  • Private session management
  • User-specific tools and credentials
  • Per-user memory and context

The key principle here: each user gets their own copy of everything. Not just their own data—their own code running in their own container. This is the nuclear option for isolation. It’s expensive, but it’s airtight.

Why would you do this? Imagine you’re running a B2B platform where company A and company B are direct competitors. Company A’s CEO is not going to sleep well knowing their AI agent training data, conversation history, and learned models could theoretically leak to Company B through a shared instance. Per-user workspaces eliminate this concern entirely because there’s literally no shared code path. It’s like giving each user their own server.

Here’s what that looks like in practice:

# docker-compose.yml - Multi-workspace OpenClaw
version: "3.8"

services:
  # User A's workspace
  openclaw-user-a:
    image: openclaw:latest
    environment:
      WORKSPACE_ID: user-a
      DATA_STORE: postgres://db:5432/openclaw_user_a
      MEMORY_PATH: /mnt/workspaces/user-a/memory
      SESSION_KEY: ${USER_A_SESSION_KEY}
    volumes:
      - workspace-a:/mnt/workspaces/user-a
    networks:
      - user-a-network

  # User B's workspace
  openclaw-user-b:
    image: openclaw:latest
    environment:
      WORKSPACE_ID: user-b
      DATA_STORE: postgres://db:5432/openclaw_user_b
      MEMORY_PATH: /mnt/workspaces/user-b/memory
      SESSION_KEY: ${USER_B_SESSION_KEY}
    volumes:
      - workspace-b:/mnt/workspaces/user-b
    networks:
      - user-b-network

volumes:
  workspace-a:
  workspace-b:

networks:
  user-a-network:
  user-b-network:

What you’re seeing here: complete isolation. User A’s OpenClaw instance can’t even see User B’s data. They have different database credentials. Different file paths. Different networks. Different session tokens.

User A’s container talks to its own database. User B’s container talks to a completely different database. If there’s a SQL injection vulnerability in OpenClaw, an attacker can only access the data in that user’s database—not all users. If there’s a bug in memory management, it only affects that user’s memory.

This is simple. It’s also expensive—you’re running N copies of OpenClaw for N users. But it’s safe.

Let’s talk about the resource implications. Each workspace runs its own agent instances, holds its own memory, maintains its own connections to its database. If you’ve got 50 users, you’re running 50 containerized instances. On a modern VPS with 8 cores and 32GB RAM, you might fit 5-10 user instances before you hit resource limits. That means either multiple VPS, or a move to Kubernetes for orchestration.

But here’s the thing: it scales horizontally in a clean way. You can’t optimize away the fundamental problem—User A and User B need their own instances. But you can throw more hardware at it. Spin up more VPS, use Kubernetes with autoscaling, pay the Tailscale network overhead to connect them (or use a private network). The architecture doesn’t change. It just gets bigger.

The cost math: if each instance takes 500MB of RAM, you need 25GB for 50 users. If you use a $50/month VPS with 32GB RAM, you might fit 60 users. At $1 per user per month, you’re profitable if you charge $5+. This is why startups using this pattern charge what they charge.

Pattern 2: Shared Infrastructure with Permission Gates

Now you want to be smarter about resources. You want to run one OpenClaw backend and have multiple users access it concurrently, but with strict permission boundaries. This is doable, but it requires adding a permission layer that OpenClaw doesn’t have by default.

This is where most teams land because it’s the middle ground: better resource efficiency than Pattern 1, stronger isolation than just a database schema. You get decent isolation for modest infrastructure spend.

Here’s the architecture:

User Request (from phone/web)
    ↓
[API Gateway - Authentication & Token Validation]
    ↓
[Permission Layer - "Which user is this? What can they do?"]
    ↓
[OpenClaw Agent - Per-Request Context Isolation]
    ↓
[Database Access Control - Row-Level Security]
    ↓
[Response - Filtered to show only user's data]

The flow is: request comes in, we figure out who the user is (via JWT token or session), we tag their request with that context, OpenClaw processes it while respecting that context, and the database enforces it as a final safety check.

Let’s break each layer down:

Layer 1: API Gateway as Trust Boundary

Your API gateway receives a request. It extracts the user’s identity from the session token, JWT, or API key. It validates that the user is authenticated. Then—and this is the critical part—it doesn’t pass raw requests to OpenClaw. It wraps the request with permission context.

# Pseudo-code: API Gateway
@app.post("/agent/run")
def run_agent(request: AgentRequest, user: User):
    # Extract user identity
    user_id = user.id
    workspace_id = user.workspace_id

    # Wrap request with permission context
    wrapped_request = {
        "agent_id": request.agent_id,
        "input": request.input,
        "context": {
            "user_id": user_id,
            "workspace_id": workspace_id,
            "allowed_tools": get_user_tools(user_id),
            "data_scope": get_user_data_scope(user_id),
        }
    }

    # Send to OpenClaw with context
    result = openclaw.run_agent(wrapped_request)
    return result

The key insight: the permission context travels with the request. OpenClaw doesn’t need to know about users or permissions. It just needs to respect the context passed to it.

Layer 2: Per-Request Context Isolation in OpenClaw

When OpenClaw receives the wrapped request, it extracts the permission context and uses it to filter everything downstream. Memory access, tool execution, data retrieval—all filtered by the user’s permissions.

# Pseudo-code: OpenClaw Agent Execution
class ContextualAgent:
    def __init__(self, context):
        self.context = context
        self.user_id = context["user_id"]
        self.allowed_tools = context["allowed_tools"]

    def execute_tool(self, tool_name, params):
        # Check if user can execute this tool
        if tool_name not in self.allowed_tools:
            raise PermissionError(f"User {self.user_id} cannot use {tool_name}")

        # Execute with user context
        return tool_registry.execute(tool_name, params, user_id=self.user_id)

    def access_memory(self, memory_key):
        # Fetch only this user's memory
        return memory_store.get(key=memory_key, user_id=self.user_id)

The agent becomes a “context-aware agent.” It knows which user it’s serving and restricts its actions accordingly.

Layer 3: Row-Level Security in the Database

Finally, the database enforces the boundary. Every table has a user_id or workspace_id column. Every query is automatically scoped.

-- Database-level enforcement
-- When User A queries "SELECT * FROM conversations"
-- The database applies: WHERE workspace_id = 'user-a'

-- Even if User A somehow crafted a query to bypass the application layer,
-- the database policy prevents access to other workspaces

CREATE POLICY user_workspace_isolation ON conversations
  USING (workspace_id = current_setting('app.workspace_id'));

This is defense in depth. The application layer enforces permissions. The database layer enforces them again. If one fails, the other catches it.

The trade-off: complexity. You’re adding permission layers, context plumbing, and database policies. You need to test that this actually works—write integration tests that verify User A really can’t read User B’s data even if they try.

Pattern 3: Separate Gateways for Adversarial Users

Now we’re talking about a scenario where users might be actively hostile. Maybe you’re running a B2B SaaS platform and your customers’ competitors could be users too. Maybe you’re running a government agency where different departments have conflicting interests. Maybe you handle extremely sensitive data and you’re paranoid, which is reasonable.

In this case, you don’t trust a single permission layer to hold. You want actual network-level isolation enforced by the infrastructure itself.

The idea here is to split each user’s gateway onto its own Docker network. That network can only be accessed by that user’s code. Even if there’s a vulnerability that lets code escape the OpenClaw process, that code can’t reach another user’s resources because they’re on a different network.

# docker-compose.yml - Multi-gateway approach with network isolation
version: "3.8"

services:
  # User A's gateway + agent (separate network, completely isolated)
  gateway-user-a:
    image: openclaw-gateway:latest
    environment:
      WORKSPACE_ID: user-a
      BACKEND_URL: http://openclaw-user-a:8000
      # Important: only internal network, no external access
    networks:
      - gateway-network-a
    # No ports exposed on host—only reachable through load balancer with auth
    # ports:
    #   - "8001:8000"

  openclaw-user-a:
    image: openclaw:latest
    environment:
      WORKSPACE_ID: user-a
      DATABASE_URL: postgresql://user-a:password@postgres:5432/openclaw
    networks:
      - gateway-network-a # Only reachable from gateway-a
    # No direct external access

  # User B's gateway + agent (completely separate network)
  gateway-user-b:
    image: openclaw-gateway:latest
    environment:
      WORKSPACE_ID: user-b
      BACKEND_URL: http://openclaw-user-b:8000
    networks:
      - gateway-network-b
    # No ports exposed

  openclaw-user-b:
    image: openclaw:latest
    environment:
      WORKSPACE_ID: user-b
      DATABASE_URL: postgresql://user-b:password@postgres:5432/openclaw
    networks:
      - gateway-network-b # Only reachable from gateway-b

  # Load balancer in front (on default network)
  # Handles auth, routing to correct gateway
  load-balancer:
    image: traefik:latest
    networks:
      - default
      - gateway-network-a
      - gateway-network-b
    ports:
      - "443:443"
    volumes:
      - ./traefik-config.yaml:/traefik-config.yaml

  # Shared database (with row-level security as extra safety)
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_INITDB_ARGS: "-c shared_preload_libraries=pg_audit"
    networks:
      - default # Connected to load balancer only for backups/admin
    volumes:
      - db-data:/var/lib/postgresql/data

networks:
  gateway-network-a:
    # Completely isolated network, no external access
  gateway-network-b:
    # Completely isolated network, no external access
  default:
    # Load balancer network

volumes:
  db-data:

What’s happening here is network-level enforcement. User A’s gateway and OpenClaw instance exist on gateway-network-a. User B’s code exists on gateway-network-b. These are separate Docker networks with no routes between them. Even if User A’s OpenClaw process somehow executes arbitrary code (maybe an agent runs untrusted Python), that code can’t make network requests to User B’s resources. They’re not on the same network.

The load balancer (Traefik or nginx) sits in front on the default network. It has network access to both user networks. The load balancer handles authentication—it verifies the user’s JWT or session token, then routes their request to the correct gateway.

Even better: you could deploy User A and User B on completely separate VPS with separate Tailscale networks. Then they don’t share infrastructure at all.

This is expensive in terms of infrastructure, but it’s extremely strong. A compromised User A agent can’t pivot to User B’s resources because there’s literally no network path. Even if database row-level security fails, the agent can’t reach User B’s database because it’s not on the network.

The gateway can add additional logging, rate limiting, and request filtering specific to each user. If User A is misbehaving (running expensive agents, making thousands of requests), you can rate-limit their gateway without affecting User B.

Use this pattern when:

  • Users are fundamentally adversarial (competing companies, conflicting departments)
  • Compliance requires strict isolation (HIPAA, PCI-DSS, SOC 2 Type II, etc.)
  • Data sensitivity is extremely high (healthcare, finance, government)
  • You can afford the infrastructure cost (multiple gateways, separate networks)
  • A security breach would be catastrophic

Session and Memory Isolation: What’s Automatic, What’s Not

Here’s a gotcha that catches people: memory isolation isn’t automatic just because you’ve got separate databases.

OpenClaw’s memory system stores learned preferences, context, and history. If you’ve got shared agents (Pattern 2), and those agents accumulate memory across requests, whose memory is it? User A’s agent learns that User A likes concise reports. User B’s agent learns that User B likes detailed analysis. If they’re the same agent instance, which preference wins?

The answer: nobody’s preference wins, and you’ve got a bug.

Here’s why this matters. OpenClaw’s agents are stateful. They learn over time. If you run the same agent instance for multiple users, the agent’s internal state becomes a conflicting mess. The agent might recommend action X because User A trained it to prefer X. But now User B is using the same agent, and X is terrible for User B. The agent doesn’t know that User B is different from User A because there’s no user concept in the agent.

This is why memory scoping is critical. Every bit of data the agent learns needs to be tagged with whose user it belongs to. Here’s how to handle it:

# Pattern: User-scoped memory
class UserScopedMemory:
    def __init__(self, memory_backend, user_id):
        self.backend = memory_backend
        self.user_id = user_id

    def write(self, key, value):
        # Store with user prefix to keep memory isolated
        scoped_key = f"{self.user_id}:{key}"
        return self.backend.write(scoped_key, value)

    def read(self, key):
        # Fetch only this user's memory, not other users'
        scoped_key = f"{self.user_id}:{key}"
        return self.backend.read(scoped_key)

    def list_keys(self, prefix=""):
        # List only keys matching this user
        scoped_prefix = f"{self.user_id}:{prefix}"
        return self.backend.list_keys(scoped_prefix)

Every memory operation is prefixed with the user ID. User A’s memory lives in user-a:conversation-history. User B’s memory lives in user-b:conversation-history. Even if they’re in the same Redis instance or same database, they’re logically isolated. The key namespace prevents cross-contamination.

But here’s the critical part: every agent execution needs to pass the user ID when accessing memory. This is where bugs happen. You forget to pass the user ID, and suddenly all users share memory. Always make user ID a required parameter, not optional.

Session Tokens and Revocation

Here’s the warning: session tokens need to be long-lived and revocable. A session token is how OpenClaw knows which user it’s serving. If you lose track of sessions—issue tokens that never expire, or don’t revoke them when users leave—then someone could take a token, use it later, and have full access to that user’s data even after they’ve left the organization.

Imagine Alice leaves your company. On her last day, you disable her account in your system. But Alice saved her session token somewhere (maybe in a shell history, or a script she wrote). A week later, Alice’s replacement Bob logs in. But Alice, using her old token, can still access Bob’s workspace because the token is still valid. This is a serious data leak.

Implement session management:

# Session management with revocation
class SessionManager:
    def issue_token(self, user_id, expiry_hours=24):
        token = generate_secure_token()
        store_session(token, {
            'user_id': user_id,
            'issued_at': now(),
            'expires_at': now() + timedelta(hours=expiry_hours),
            'status': 'active'
        })
        return token

    def validate_token(self, token):
        session = fetch_session(token)
        if session and session['status'] == 'active' and session['expires_at'] > now():
            return session['user_id']
        return None

    def revoke_token(self, token):
        session = fetch_session(token)
        if session:
            session['status'] = 'revoked'
            update_session(token, session)

    def revoke_user_tokens(self, user_id):
        # When user leaves, revoke all their active tokens
        sessions = fetch_sessions_for_user(user_id)
        for token in sessions:
            self.revoke_token(token)

When a user leaves, call revoke_user_tokens(alice_id) to invalidate all her sessions. This costs you one database lookup per request (to validate the token), but it’s non-negotiable for security. You can optimize with caching: cache valid tokens in Redis with a TTL, and only hit the database if the token isn’t in cache.

This is also important for suspicious activity. If User A’s account starts behaving weird (thousands of requests, unusual times, different IP addresses), you can revoke all their tokens and force them to re-authenticate. This stops the compromised session immediately while giving you time to investigate.

Permission Matrices: Thinking About Tool Access

Now we get into the specifics: which users can use which tools? This is where you define who can do what.

You could hardcode permissions directly (“User A is an admin, gets all tools”). But that doesn’t scale and creates a maintenance nightmare. Instead, implement role-based access control (RBAC):

# RBAC: Role-Tool Mapping with granular actions
roles:
  admin:
    tools:
      - web_scraper: {} # All actions
      - email_send: {}
      - calendar_write: {}
      - database_query:
          actions:
            - select
            - insert
            - update
            # Note: delete is NOT allowed for anyone

  analyst:
    tools:
      - web_scraper: {} # Can scrape
      - calendar_read: {} # Can read calendar
      - database_query:
          actions:
            - select # Read-only

  operator:
    tools:
      - email_send:
          rate_limit: 100/hour
      - calendar_write:
          rate_limit: 50/day

users:
  [email protected]:
    role: admin
    workspace: company-main
    # Alice can do anything

  [email protected]:
    role: analyst
    workspace: company-main
    # Bob can research but not modify systems

  [email protected]:
    role: operator
    workspace: company-main
    # Charlie can execute specific tasks with rate limits

  [email protected]:
    role: analyst
    workspace: company-main
    # Eve, the intern, has same read-only access as Bob

The gateway looks up the user’s role, fetches the allowed tools and actions, and passes that context to the agent. The agent can’t execute actions outside its permission scope. Simple, scales to hundreds of users, and easy to audit: “What tools can role X use?”

But there’s a subtlety: tools themselves might have varying permission levels and constraints. A database_query tool could be restricted to read-only queries. A file_write tool could be restricted to a specific directory. A send_email tool might have rate limits. You might implement this as:

# Tool-level permission checks with action-level granularity
class RestrictedTool:
    def __init__(self, name, config):
        self.name = name
        self.config = config

    def can_execute(self, user_id, action, params):
        # Check if user has this tool at all
        if self.name not in get_user_tools(user_id):
            return False, "Tool not available for user"

        # Check if user can perform this specific action
        allowed_actions = self.config.get('allowed_actions', [])
        if allowed_actions and action not in allowed_actions:
            return False, f"Action {action} not allowed"

        # Check rate limits
        if 'rate_limit' in self.config:
            limit = self.config['rate_limit']
            current_usage = get_usage_this_hour(user_id, self.name)
            if current_usage >= limit:
                return False, "Rate limit exceeded"

        return True, None

    def execute(self, user_id, action, params):
        can_exec, reason = self.can_execute(user_id, action, params)
        if not can_exec:
            raise PermissionError(reason)

        # Execute in sandboxed environment with user context
        log_tool_execution(user_id, self.name, action, params)
        result = self._execute_sandboxed(action, params, user_id)
        log_tool_result(user_id, self.name, action, result)
        return result

This is more complex than simple role-based access, but it gives you fine-grained control. You can prevent dangerous actions (delete database records), enforce rate limits (prevent spam), and audit everything (log who did what).

For most use cases, role-based access at the tool level is sufficient. But if you need to prevent specific dangerous actions or enforce usage limits, implement action-level checks.

Audit Logging: Knowing What Happened

Here’s the unglamorous part of multi-user systems: you need to know what happened, when, and who did it. Audit logging is non-negotiable. It’s your evidence in compliance audits, your tool for incident response, and your proof that you didn’t do something stupid.

The principle: every action by every user gets logged to an immutable record.

Implement audit logging:

# Comprehensive audit logging
def log_agent_execution(user_id, agent_id, action, input_data, result, error=None, timestamp=None):
    import json
    from datetime import datetime

    if timestamp is None:
        timestamp = datetime.utcnow().isoformat()

    audit_entry = {
        'timestamp': timestamp,
        'user_id': user_id,
        'agent_id': agent_id,
        'workspace_id': get_workspace_for_user(user_id),
        'action': action,

        # Don't store raw sensitive params; hash them for verification
        'input_hash': hash_data(json.dumps(input_data, sort_keys=True)),
        'input_size_bytes': len(json.dumps(input_data)),

        # Result details
        'result_status': result.status if result else 'error',
        'result_data_hash': hash_data(json.dumps(result.data)) if result else None,

        # Error tracking
        'error_type': type(error).__name__ if error else None,
        'error_message': str(error) if error else None,

        # User environment
        'user_ip': get_request_ip(),
        'user_agent': get_request_user_agent(),
    }

    # Write to append-only log (never update, only append)
    write_to_audit_log(audit_entry)

    # Also write to structured logging (for monitoring)
    log_to_structured_sink(audit_entry)

Store logs in a write-only format: append-only log. Don’t make audit entries updatable or deletable. Include user ID, timestamp, what action was taken, the result, and the user’s IP. Never log sensitive data—hash parameters instead of storing them plaintext.

Why hash instead of storing? If you store API keys, passwords, or personal data in logs, you’ve just created another attack surface. An attacker who compromises your log storage has your secrets. By hashing, you can still verify “the user did something” without exposing the secret itself.

Querying Audit Logs for Incident Response

When something goes wrong, audit logs are your tool for understanding what happened:

# Incident response queries
def investigate_user(user_id, start_date, end_date):
    """What did this user do during this period?"""
    return query_audit_log(
        user_id=user_id,
        timestamp_gte=start_date,
        timestamp_lte=end_date
    )

def find_suspicious_activity(user_id, hours=24):
    """Unusual patterns: many failures, unusual times, high rate"""
    return query_audit_log(
        user_id=user_id,
        timestamp_gte=now() - timedelta(hours=hours),
        filters=[
            ('result_status', '=', 'error'),  # Many failures?
            ('action_count_per_minute', '>', 10),  # Rapid-fire actions?
        ]
    )

def find_data_access_pattern(resource_id):
    """Who accessed this data and when?"""
    return query_audit_log(
        action='access_resource',
        filters=[('resource_id', '=', resource_id)]
    )

Audit logs answer the critical questions in incident response:

  • “What did User A actually do?”
  • “When was resource X last accessed?”
  • “Who could have seen this data?”
  • “Is this activity normal for this user?”

This is also critical for compliance audits. When your auditor asks “Can you prove User A didn’t access User B’s data?”, you query the audit log and show that User A never attempted to access User B’s resources.

Practical Example: Deploying for a Team of Five

Let’s put this together. You’ve got a team of five people: Alice (senior engineer, needs everything), Bob (junior, limited tools), Charlie (product manager, specific workflows), Diana (ops, monitoring and admin), and Eve (intern, read-only access). You want to deploy OpenClaw for them using Pattern 2 (shared infrastructure with permission gates) because they trust each other and you don’t have extreme compliance requirements.

# docker-compose.yml - Team deployment
version: "3.8"

services:
  # API Gateway (single entry point)
  gateway:
    image: openclaw-gateway:latest
    environment:
      OPENCLAW_BACKEND: http://openclaw:8000
      SESSION_STORE: redis://redis:6379
    ports:
      - "8000:8000"
    depends_on:
      - openclaw
      - redis
    volumes:
      - ./config/rbac.yaml:/config/rbac.yaml

  # OpenClaw Backend
  openclaw:
    image: openclaw:latest
    environment:
      DATABASE_URL: postgresql://postgres:password@postgres:5432/openclaw
      REDIS_URL: redis://redis:6379
      LOG_LEVEL: info
    depends_on:
      - postgres
      - redis
    volumes:
      - ./config/agents.yaml:/config/agents.yaml

  # Database (shared, row-level security)
  postgres:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: openclaw
      POSTGRES_PASSWORD: password
      POSTGRES_INITDB_ARGS: "-c shared_preload_libraries=pg_audit"
    volumes:
      - ./sql/init-rls.sql:/docker-entrypoint-initdb.d/01-rls.sql
      - db-data:/var/lib/postgresql/data

  # Redis (session store, memory cache)
  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis-data:/data

volumes:
  db-data:
  redis-data:

And the init script for row-level security:

-- sql/init-rls.sql
ALTER TABLE conversations ENABLE ROW LEVEL SECURITY;
CREATE POLICY conversations_isolation ON conversations
  USING (user_id = current_setting('app.user_id'))
  WITH CHECK (user_id = current_setting('app.user_id'));

ALTER TABLE memory ENABLE ROW LEVEL SECURITY;
CREATE POLICY memory_isolation ON memory
  USING (user_id = current_setting('app.user_id'))
  WITH CHECK (user_id = current_setting('app.user_id'));

ALTER TABLE tool_executions ENABLE ROW LEVEL SECURITY;
CREATE POLICY tool_executions_isolation ON tool_executions
  USING (user_id = current_setting('app.user_id'))
  WITH CHECK (user_id = current_setting('app.user_id'));

Deploy it. Your five team members each get a login. The gateway authenticates them, wraps their requests with user context, and OpenClaw serves them isolated environments on shared infrastructure.

The Gotchas You’ll Hit

After you deploy this, you’ll run into issues. I’m telling you now so you recognize them:

  1. Tool credentials leak into shared memory: You configure a tool with an API key. That key gets cached in memory. User B, running a different agent, somehow accesses memory entries from User A’s agent execution. Now User B has User A’s API credentials. Solve this by: (a) never storing credentials in memory, (b) using a separate, encrypted credential store, (c) rotating credentials frequently.

  2. Race conditions on concurrent requests: Two users run agents simultaneously. They both try to access shared agent instances. The agent state gets confused. Solve this by: (a) using per-request context instead of shared state, (b) running agents in stateless mode, (c) implementing request queuing.

  3. Memory “leaks” between users: OpenClaw learns something about User A (e.g., they prefer JSON output). Later, when User B uses an agent, that preference persists. The agent gives User B JSON output even though User B prefers YAML. This is caused by shared agent instances with persistent memory. Solve this by: (a) scoping memory to the user, (b) separating agent instances per user, (c) clearing memory between requests.

  4. Compliance audits fail: You get audited and realize you’re not logging what you should be. You can’t prove that User A didn’t access User B’s data because you didn’t log agent executions. Start logging everything from day one.

When to Choose Each Pattern

  • Pattern 1 (Full Isolation): You’re serving high-sensitivity data, users are adversarial, or compliance requires it. You have the infrastructure budget.
  • Pattern 2 (Shared with Permission Gates): You’ve got modest resource constraints, users are generally trustworthy, and you don’t have extreme compliance requirements. You implement careful permission layers.
  • Pattern 3 (Separate Gateways): You’re running a high-risk multi-tenant SaaS where compromise is catastrophic. You have serious infrastructure and ops budget.

Or combine them: use Pattern 3 for your highest-value customers, Pattern 2 for standard customers, Pattern 1 for internal use.

When to Graduate from One Pattern to Another

As your team grows, you might start with Pattern 2 and eventually move to Pattern 3. Here’s the thinking:

Start with Pattern 2 (5-20 users)

  • You trust your team
  • Compliance requirements are light to moderate
  • You want to optimize costs
  • You’ve got a single VPS or cloud instance

Move to Pattern 3 (20-100+ users)

  • You’re handling high-sensitivity data
  • Customers have compliance requirements
  • You’re starting to hit resource limits
  • Security incidents become a real concern

Consider Pattern 1 (always)

  • You’re in regulated industries (healthcare, finance)
  • Each user is a paying customer with high expectations
  • The cost of a data breach is catastrophic
  • You have the infrastructure budget

The progression is usually: single-user → Pattern 2 → Pattern 3 as you scale. Or, if you’re starting a regulated business, go straight to Pattern 1 because you can’t cut corners.

Closing Thoughts

Multi-user OpenClaw isn’t a trivial undertaking, but it’s completely doable. The key is understanding that you’re adding a trust boundary layer on top of OpenClaw’s single-user core. You’re not changing OpenClaw—you’re wrapping it.

Think about where your trust boundaries actually are. Who needs to be isolated from whom? What’s the cost of a breach? How much infrastructure can you afford? Answer those questions, pick your pattern, and implement it carefully.

And for the love of all that’s holy, implement audit logging from day one. You’ll thank me when you need it. Audit logging isn’t just for compliance—it’s for your own sanity when something goes wrong. “What did User A actually do?” should be answerable with a single query.

Also, test your isolation. Don’t just assume row-level security works. Write integration tests that try to access User B’s data as User A. Try to craft queries that bypass the permission layer. Try to access the database directly. This is the kind of testing that catches mistakes before they become breaches.

Keep shipping. And keep your users’ data safe.

Free Discovery Call

Start With a Conversation, Not a Commitment

Every engagement begins with a free 30-minute discovery call. We'll map what's slowing your business down and tell you exactly what we'd fix first – no pitch deck, no obligation.