Embedding & Multi-tenancy
Use a scope to separate an agent's work by tenant, user, workspace, or record.
| Value | Purpose |
|---|---|
scope_id | Stable application identifier used to partition chat-session memory, stores, and credentials. |
session_id | One conversation. Send it to continue that conversation. |
context | Application facts for the turn. The React widget calls this prop scopeContext. |
Several conversations can share one scope. Reusing a scope does not resume a conversation.
Authenticate the scope
A scope partitions data, but it does not enforce access to existing sessions. The hosted history API accepts a caller-supplied scope filter, and chat can resume a session without comparing its scope with the token's scope. A scoped runtime token alone does not isolate tenants.
For a multi-tenant product, route runtime calls through your application backend:
- Authenticate each client request with your application's login.
- Authorize the requested scope and operation. For an existing session, check that the user can access that session in that scope. Apply these checks to chat, history, confirmations, and session actions.
- With the default platform JWT verifier, mint a short-lived runtime token on your backend using
POST /api/agents/{agent_id}/tokens. Include the authorized scope and application facts:
{
"ttl": 300,
"scope_id": "tenant:acme",
"context": {
"tenant_id": "acme",
"plan": "premium"
}
}- Keep the runtime token on your backend and use it to forward the authorized request. Record each created session's scope and ownership before allowing it to be resumed.
- Expose only the methods and paths your application needs. Set history filters on the backend and check session ownership before reads, changes, deletions, and actions. Reject unsupported paths.
Keep all runtime credentials on the backend, including short-lived tokens. Returning one to the browser lets the caller bypass your proxy's checks. Tokens minted by an automation token do not establish a separate product-user identity.
Direct browser calls to the runtime are appropriate only when callers are trusted to access the agent's sessions across scopes, such as an internal team app. Hosted team login grants agent access; it does not isolate customers within that agent.
Hosted JWKS federation has a separate verifier and membership requirements. A product JWT signed with your own shared secret does not automatically work on a hosted chat URL. See Authentication.
Choosing a scope ID
| Application feature | Example |
|---|---|
| Tenant support assistant | tenant:acme |
| Case review | case:merchant-123 |
| Portfolio assistant | account:42 |
Use stable identifiers rather than display names. Renaming a customer should not change its data partition.
Claim and request precedence
A runtime-compatible verified JWT may carry these claims:
{
"sub": "user-456",
"scope_id": "tenant:acme",
"context": { "tenant_id": "acme" }
}Chat requests can also supply scope and context:
{
"message": "Show open support tickets",
"scope_id": "tenant:acme",
"context": { "page": "support" }
}A verified JWT scope_id overrides the request body's value. Without that claim, the runtime accepts the body's scope. Your backend must authorize and set the scope on every forwarded request. This precedence does not authorize access to a resumed session.
Context merges per key: claim values win on conflict, and other body keys remain. Those body values are untrusted. Do not use the merged scopeContext alone to authorize an operation.
React integration
Point the widget at your application proxy. The proxy serves the runtime paths used by the widget and preserves streaming responses. It authenticates the application token returned by getAppToken and adds its own runtime credential to authorized upstream calls.
import { ChatWidget } from '@amodalai/react/widget';
import '@amodalai/react/widget/style.css';
function AgentPanel({ tenantId, getAppToken }: { tenantId: string; getAppToken: () => Promise<string> }) {
return (
<ChatWidget
key={tenantId}
serverUrl="https://app.example.com/api/agent"
scopeId={`tenant:${tenantId}`}
scopeContext={{ tenant_id: tenantId }}
getToken={getAppToken}
/>
);
}The application token authenticates the browser to your backend. The user widget prop does not send an identity. A key tied to the scope remounts the widget when the tenant changes; your backend still checks every request.
For an internal Amodal-hosted app whose users may access the agent's sessions across scopes, use same-origin calls and omit getToken. See Custom Runtime Apps.
Continue a conversation
A chat request without session_id starts a session. To keep one conversation across navigation, save its ID and pass it back when the panel remounts:
import { useState } from 'react';
import { ChatWidget } from '@amodalai/react/widget';
function ScopedChat({
userId,
scopeId,
getAppToken,
}: {
userId: string;
scopeId: string;
getAppToken: () => Promise<string>;
}) {
const storageKey = `amodal-session:${userId}:${scopeId}`;
const [resumeSessionId] = useState(() => localStorage.getItem(storageKey));
return (
<ChatWidget
serverUrl="https://app.example.com/api/agent"
scopeId={scopeId}
getToken={getAppToken}
resumeSessionId={resumeSessionId ?? undefined}
onStateChange={({ sessionId }) => {
if (sessionId) localStorage.setItem(storageKey, sessionId);
}}
/>
);
}Mount ScopedChat with a key containing the signed-in user and scope. Its state initializer runs on mount. Clear saved conversation references on logout when a browser is shared. The storage key organizes client state. The backend must authorize the saved session before forwarding a resume or history request.
For a custom chat interface, read the session ID from the stream's init event. Send it as session_id to streamChat, or as sessionId in RuntimeClient.chatStream options. Load prior messages with getSessionHistory. Use the proxy URL and application authentication for these calls too.
Scoped resources
| Resource | Chat-session behavior |
|---|---|
| Sessions | Labeled with a scope. History filters and resume do not enforce caller scope; the application backend must authorize access. |
| Memory | Stored and recalled within the session's scope. |
| Stores | Agent store tools use the session's scope. Stores marked shared use the global partition and are read-only through scoped tools. |
| Credentials | scope:KEY resolves a secret for the current scope. |
Without a scope, chat uses the empty-string partition. This can be useful for a single-tenant agent, but does not separate tenants.
The direct /api/stores routes and React store hooks use the global partition. They do not inherit ChatWidget.scopeId or provide tenant-scoped access. Use scoped agent tools or an application backend that enforces the required partition for a multi-tenant data UI.
requireScope
In amodal.json:
{
"scope": { "requireScope": true }
}Chat routes using a static bundle reject an empty resolved scope. The hosted runtime loads bundles dynamically and does not apply this check. For hosted integrations, have your backend authorize the scope on every request and include it in the runtime tokens it retains. This setting does not authorize session access or protect the direct store API.
Shared stores
For reference data available to every chat scope, set shared in the store definition:
{
"name": "product-catalog",
"shared": true,
"entity": {
"name": "Product",
"key": "{sku}",
"schema": {
"sku": { "type": "string" },
"name": { "type": "string" }
}
}
}See Stores for populating and using shared data.
Context injection
A connection can copy scope context into outbound HTTP requests. In its spec.json:
{
"baseUrl": "https://api.example.com",
"auth": { "type": "bearer", "token": "env:APP_API_TOKEN" },
"contextInjection": {
"tenant_id": {
"in": "header",
"field": "X-Tenant-Id",
"required": true
}
}
}The request includes X-Tenant-Id from scopeContext.tenant_id. A missing required value fails the request.
The generic request path supports header, query, path, and body injection. Native OpenAPI connections support header, query, and body injection; path injection fails preparation. Native query injection supplies defaults that model arguments can override. Enforce tenant authorization in the receiving API. See Connections.
Reading scope context in a tool
A custom tool can read the same context through ctx.scopeContext. Use it for application facts such as the current page or record. Use verified caller identity for permission checks.
Per-scope credentials
Reference a scope-specific secret in a connection:
{
"auth": { "type": "bearer", "token": "scope:USER_API_TOKEN" }
}The credential resolver looks up USER_API_TOKEN for the session's scope. In hosted deployments, manage scoped secrets through PUT /api/agents/{agent_id}/scopes/{scope_id}/secrets. Keep this operation on your backend.
A custom runtime host must supply a credential resolver for per-scope secrets. The runtime does not load .amodal/scopes.json automatically.
Inspect scope usage
Sessions provides scope filters and displays scope IDs in session details. Cost shows usage by scope. Amodal treats scope IDs as opaque strings; your application owns the mapping to customer or workspace names.