Identity from a session JWT
SSR pattern — verify your app's session JWT, derive the caller's (namespace, agent), and run each request under that scope with act_as.
Brain's identity is a (namespace, agent) pair carried by the API key; the
server derives it from the credential and clients never send a scope. There are
two ways to give each user their own slice of memory:
- Per-tenant keys — one key per
(namespace, agent), minted in the Arc Labs dashboard (see API keys) and stored encrypted. Simple, but you manage N secrets. act_asfront door (shown here) — hold one pooled service key that has been grantedcan_act_as, and stamp each request with the effective(namespace, agentId)you derived from the JWT. One secret, many tenants.
The act_as grant is a wire-protocol capability, so this pattern uses the
wire client (BrainClient), which multiplexes many requests over one
pooled connection.
Verify the JWT, derive the scope
Your app already validates the session JWT. Map its claims to a Brain scope —
namespace is the company/tenant, agentId is the app/persona (a UUID):
import { jwtVerify } from 'jose';
async function scopeFromJwt(token: string): Promise<{ namespace: string; agentId: string }> {
const { payload } = await jwtVerify(token, KEY);
// Map your own claims onto Brain's identity model.
return { namespace: payload.org as string, agentId: payload.agentId as string };
}Run the request under that scope
// Construct one shared, pooled wire client at startup with the service key.
import { BrainClient, EncodeBuilder, RecallBuilder } from '@brain-db/sdk';
const brain = await BrainClient.connect('brain.internal', 8080, {
auth: { kind: 'token', token: process.env.BRAIN_SERVICE_KEY! },
});
// Next.js Route Handler — runs per request.
export async function POST(req: Request) {
const token = req.headers.get('authorization')?.replace('Bearer ', '');
if (!token) return new Response('unauthenticated', { status: 401 });
const { namespace, agentId } = await scopeFromJwt(token);
const { message } = await req.json();
// Every op runs as the effective (namespace, agentId).
await brain.encode(
new EncodeBuilder(message).actAs(namespace, agentId).build(),
);
const answer = await brain.recall(
new RecallBuilder(message).actAs(namespace, agentId).maxResults(5).build(),
);
return Response.json({ answerKind: answer.answerKind });
}# FastAPI route — one shared wire client, per-request act_as.
from fastapi import APIRouter, Header
from brain_db_sdk import BrainClient
from brain_db_sdk.verbs import EncodeBuilder, RecallBuilder
router = APIRouter()
brain = BrainClient.connect('brain.internal', 8080, {'kind': 'token', 'token': SERVICE_KEY})
@router.post('/chat')
async def chat(message: str, authorization: str = Header(...)):
namespace, agent_id = scope_from_jwt(authorization.removeprefix('Bearer '))
await brain.encode(
EncodeBuilder(message).act_as(namespace, agent_id).build()
)
answer = await brain.recall(
RecallBuilder(message).act_as(namespace, agent_id).max_results(5).build()
)
return {'answer_kind': answer.answer_kind}The service key must be granted can_act_as with an allowlist of namespaces
it may impersonate. A request whose derived namespace is outside that
allowlist is rejected — the SDK surfaces it as an act_as denial
(isActAsDenied). Keep the service key server-side; never ship it to a
browser.
Don't fall back to the service key's own scope when the JWT is missing or
invalid. A dropped act_as silently writes to the wrong tenant — the failure
mode is corrupted memory, not an error. Reject the request instead.
Was this page helpful?