Build a multi-tenant application
Use this pattern when many End-users share your Blazing Agents tenant. Your
backend remains the identity authority and maps each verified principal to an
opaque, stable userId for Attribution.
Outcome
Each authorized application chat starts with trusted Attribution, and your backend can list its Sessions and usage by the same identifier without storing a Blazing Agents End-user record.
Before you begin
Create a backend SDK client and an application-owned mapping from signed-in principals to allowed Agents and Sessions. Review Tenancy and end-user Attribution, Sessions and Turns, Usage and quotas, and Security and credentials.
Map application identity
Derive a stable opaque userId from the authenticated application
principal. Resolve agentId, the application chat, and any sessionId
through backend authorization; never copy those values from an unchecked
browser body.
import { BlazingAgents, type UIMessage } from "@blazing-agents/sdk";
const client = new BlazingAgents({ apiKey: process.env.BLAZING_AGENTS_API_KEY! });
export async function runAuthorizedTurn(input: {
principal: { subject: string; workspaceId: string };
appChatId: string;
message: UIMessage;
}) {
const userId = `app:${input.principal.subject}`;
const chat = await resolveAuthorizedChat(input.principal, input.appChatId);
const result = await client.chat({
agentId: chat.agentId,
...(chat.sessionId ? { sessionId: chat.sessionId } : {}),
message: input.message,
userId,
metadata: { workspaceId: input.principal.workspaceId },
});
const sessionId = await result.sessionId;
for await (const _chunk of result.toUIMessageStream()) { /* drain */ }
if (!chat.sessionId) await saveAuthorizedSession(input.appChatId, sessionId);
const sessions = await client.sessions.list(chat.agentId, { userId });
const usage = await client.usage.getForAgent(chat.agentId, {
userId, groupBy: "session",
});
const otherUserSessions = await client.sessions.list(chat.agentId, { userId: "app:other" });
return { sessionId, sessions, usage, otherUserSessions };
}Attach Attribution
userId and metadata stamp the new Session and this Turn's usage.
userId: "" is the tenant-level bucket; omitting the field defaults to
that value. Attribution supports filtering and reporting, but it is not
authorization and does not weaken the platform's credential-derived tenant
isolation.
Filter Sessions and usage
sessions.list(agentId, { userId }) filters that Agent's Sessions by exact
Attribution. Tenant-wide usage.get() accepts agentId, sessionId, and
userId filters. usage.getForAgent(agentId, query) takes the effective
Agent scope from its path argument; an agentId inside query is ignored.
Its effective query filters are sessionId and userId. Both usage methods
also accept from, to, groupBy, and limit; groupBy supports day,
agent, model, session, and user. Omitting userId returns all rows
visible to the tenant credential, while passing "" selects tenant-level
rows.
Verify tenant boundaries
After the stream finishes, assert that sessions.data contains
sessionId, its userId equals the derived value, and the usage totals
include the Turn. With an unused identifier, otherUserSessions.data
should be empty. Also test your application authorization layer rejects
another principal's appChatId before calling the SDK. A credential from a
different Blazing Agents tenant cannot read or resume this tenant's Session.
Production notes
- One tenant API key can reach every resource in that tenant. Enforce roles, Agent allowlists, and application-chat ownership before SDK calls.
- Keep
userIdstable and opaque. Changing the mapping splits Session and usage filters across identifiers. - Apply the same trusted Attribution to Tasks and inspect Artifact
userIdwhen exposing their results. Their tenant isolation is enforced by the credential; application-level access remains your responsibility. - Do not treat an empty filtered list as proof that a caller may access a separately supplied resource ID.