Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Embedding & Multi-tenancy

This guide is for SaaS builders who want to embed an Amodal agent in their application with per-user or per-tenant data isolation. The key primitive is scope_id — a string that partitions sessions, memory, and stores so each tenant sees only their own data.

How Scope Works

When a chat request includes a scope_id, the runtime uses it to partition every stateful resource:

  • Sessions — conversation history is isolated per scope
  • Memory — agent memories are stored and recalled per scope
  • Stores — store documents are partitioned per scope
  • Credentialsscope:KEY resolves to per-scope secrets

The embedding application controls who gets which scope_id. The runtime does not enforce authentication itself — it trusts the value it receives. Your app is the security boundary.

Passing Scope

Two methods, depending on your auth setup.

Request Body

Include scope_id and context in the POST body to /chat/stream:

{
  "message": "Show me my open support tickets",
  "scope_id": "tenant-abc-123",
  "context": {
    "tenant_id": "tenant-abc-123",
    "plan": "premium"
  }
}

JWT Claims

If using JWT authentication, include scope_id and scopeContext as claims in the token payload:

{
  "sub": "user-456",
  "scope_id": "tenant-abc-123",
  "scopeContext": {
    "tenant_id": "tenant-abc-123",
    "plan": "premium"
  }
}

JWT claims take precedence over body fields. This is the recommended approach for production — it prevents clients from spoofing scope.

React SDK

The ChatWidget accepts scopeId and scopeContext props. The SDK sends these with every chat request automatically.

import { ChatWidget } from '@amodalai/react';
 
function AgentPanel({ userId, tenantId }: { userId: string; tenantId: string }) {
  return (
    <ChatWidget
      serverUrl="https://your-agent.example.com"
      user={{ id: userId }}
      scopeId={tenantId}
      scopeContext={{ tenant_id: tenantId }}
      getToken={() => getAccessToken()}
    />
  );
}

The getToken callback should return a JWT or API key for authenticating requests to your agent server. It can be async to support token refresh.

getToken is for the case this page describes: your product owns user identity and mints a token (typically a jwt_secret/oidc end-user JWT carrying scope_id/scopeContext). If instead the agent is served by Amodal and uses hosted login (the default), auth is cookie-backed and injected at the edge — the app makes same-origin calls and omits getToken. See Authentication for the full model and how to choose.

Context Injection

Scope context values can be injected into outbound API calls automatically. This is how the agent passes tenant identifiers to your backend without the LLM needing to know about them.

Configure contextInjection in a connection's spec.json:

{
  "baseUrl": "https://api.your-app.com",
  "auth": {
    "type": "bearer",
    "token": "env:APP_API_TOKEN"
  },
  "contextInjection": {
    "tenant_id": {
      "in": "header",
      "field": "X-Tenant-Id",
      "required": true
    }
  }
}

Every API call the agent makes to this connection will include the X-Tenant-Id header, populated from the tenant_id key in scope context. If required is true and the key is missing, the request fails with an error instead of silently omitting it.

Injection targets: header, query, path, body. See Connections — Context Injection for the full reference.

What Gets Scoped

ResourceBehaviorConfigured in
SessionsEach scope gets independent conversation historyAutomatic
MemoryAgent memories are stored and recalled per scopeMemory
StoresStore documents are partitioned per scopeStores
Credentialsscope:KEY resolves per-scope secretsConnections

Without a scope_id, all requests share a single global partition (the empty-string scope). This is fine for single-tenant or development use.

Viewing Scope Usage in Amodal

Amodal uses the same scope_id values from session history for operator visibility:

  • Sessions shows scope filter chips with session counts, so you can review one tenant, user, or workspace at a time.
  • Cost & Usage shows a scope comparison card across the selected date range. Selecting a scope focuses the model, deploy, trend, and highest-cost session panels on that scope.
  • Session replay includes the raw scope ID in the session metadata.

Amodal treats scope_id as an opaque stable identifier. Your application owns the mapping from that identifier to human-readable tenant names. For example, you might pass tenant_01H... to Amodal and show "Acme Hotels" in your own admin UI. Use stable IDs instead of mutable display names so historical sessions and cost reports remain consistent if a customer renames their account.

Shared Stores

By default, stores are partitioned per scope. To make a store shared across all scopes — for example, a product catalog or reference data — add "shared": true to the store definition:

{
  "name": "product-catalog",
  "shared": true,
  "entity": {
    "name": "Product",
    "key": "{sku}",
    "schema": {
      "sku": { "type": "string" },
      "name": { "type": "string" },
      "price": { "type": "number" }
    }
  }
}

Shared stores are readable by all scopes. See Stores — Scoped vs. Shared for details.

Per-scope Credentials

Connection auth values can reference per-scope secrets using the scope:KEY prefix:

{
  "auth": {
    "type": "bearer",
    "token": "scope:USER_API_TOKEN"
  }
}

The runtime resolves scope:USER_API_TOKEN by looking up USER_API_TOKEN in the current scope's credential store.

Local development: Define scope credentials in .amodal/scopes.json:

{
  "tenant-abc-123": {
    "credentials": {
      "USER_API_TOKEN": "tok_dev_abc123"
    }
  },
  "tenant-def-456": {
    "credentials": {
      "USER_API_TOKEN": "tok_dev_def456"
    }
  }
}

Production: Credentials are managed by the platform's credential resolver, keyed by scope ID.

requireScope

In production, enable requireScope in amodal.json to reject any request that does not include a scope_id:

{
  "scope": {
    "requireScope": true
  }
}

This prevents accidental unscoped access — if a client forgets to pass a scope, the request fails immediately instead of writing to the global partition.

Example: Support Portal

A support portal where each customer workspace has its own agent scope.

amodal.json — require scope in production:

{
  "name": "support-agent",
  "version": "0.1.0",
  "scope": {
    "requireScope": true
  }
}

amodal/connections/app-api/spec.json — inject tenant_id into every API call:

{
  "baseUrl": "https://api.example.com",
  "auth": {
    "type": "bearer",
    "token": "env:APP_API_TOKEN"
  },
  "contextInjection": {
    "tenant_id": {
      "in": "header",
      "field": "X-Tenant-Id",
      "required": true
    }
  }
}

amodal/stores/preferences.json — workspace preferences, scoped by default:

{
  "name": "preferences",
  "entity": {
    "name": "WorkspacePreference",
    "key": "{key}",
    "schema": {
      "key": { "type": "string" },
      "value": { "type": "string" }
    }
  }
}

amodal/stores/categories.json — shared ticket categories across all tenants:

{
  "name": "ticket-categories",
  "shared": true,
  "entity": {
    "name": "Category",
    "key": "{slug}",
    "schema": {
      "slug": { "type": "string" },
      "label": { "type": "string" }
    }
  }
}

React embedding — pass the tenant as scope:

<ChatWidget
  serverUrl="https://support-agent.example.com"
  user={{ id: userId }}
  scopeId={tenantId}
  scopeContext={{ tenant_id: tenantId }}
  getToken={() => getTenantToken(tenantId)}
/>

With this setup:

  • Each tenant's conversations, memories, and preferences are isolated
  • The agent's API calls automatically include the tenant identifier
  • The shared ticket-categories store is readable by all tenants
  • Requests without a scope are rejected