This episode covers the user side: multi-user scenarios with per-user behavior, persona and role-based access that limit the agent's capabilities, and privacy considerations for user data from PII and data retention to isolation between sessions.

In episode 12 your agent was secure in infrastructure: secrets stored externally, traffic over HTTPS, and webhooks verified. But who uses that agent? So far we have assumed there is one same user in every conversation. Episode 13 destroys that assumption: you will learn multi-user scenarios, agent behavior that adapts to the user, persona and role-based access, and user data privacy.
Here is the roadmap for this episode: mapping multi-user scenarios and per-user state, defining personas along with role-based access control, and then organizing data privacy from PII to retention.
A production agent is rarely used by one person. Imagine one "checkout assistant" agent used by every customer, or one "ops" agent used by the whole engineering team. Each user has a different context, preferences, and history — and you must not mix them up.
The basic key: every turn must know who the caller is. This identity becomes part of the agent context, used to lock down sessions, memory, and tool results:
userId, not one global key.userId from context, not from a global assumption.The authentication flow in front of the agent looks like this:
export function resolveUser(req) {
const token = req.headers.authorization?.replace("Bearer ", "");
const claims = verifyJwt(token);
return {
id: claims.sub,
role: claims.role,
sessionKey: `session:${claims.sub}`,
preferredLang: claims.locale ?? "id",
};
}The token here is issued by an identity provider (Keycloak, Authentik, Authelia — see the SSO series on this blog), and the agent trusts the claims inside it. The agent itself does not need to manage passwords; its job is only to map claims to behavior.
Once the identity is clear, the agent can behave differently between users without changing code. Several things can be differentiated:
An example implementation is injecting the user profile into the system prompt when building the prompt on every turn:
export function buildSystemPrompt(user, persona) {
return [
persona.systemPrompt,
`Kamu sedang melayani ${user.id} (role: ${user.role}).`,
"Gunakan bahasa Indonesia kecuali user bicara bahasa lain.",
user.frequentTopics?.length
? `User sering bertanya soal: ${user.frequentTopics.join(", ")}.`
: "User baru pertama kali; jelaskan lebih detail.",
].join("\n");
}What must be safeguarded: personalization must not make the agent leak data between users. If a tool looks up user data, it must always be filtered by the userId from context — not from the prompt text, which can be manipulated.
Persona defines the agent's personality, style, and behavioral limits. Role-based access restricts which tools may be used based on the user's role. Both are combined through a declarative definition:
personas:
support:
description: "Asisten support yang sabar, jelas, dan to the point"
temperature: 0.4
allowedTools:
- search_knowledge_base
- read_ticket
- reply_ticket
disallowedTools:
- refund_order
- grant_access
admin:
description: "Operator yang boleh mengubah konfigurasi dan akses"
temperature: 0.2
allowedTools:
- search_knowledge_base
- read_ticket
- reply_ticket
- refund_order
- grant_accessPersona selection happens at runtime based on the user's role and request type. The important consequence: the chosen persona must reject tools outside the allowedTools list at the enforcement level — not just in the prompt. A user with the support role could write "do a refund", but the agent does not have a refund_order tool to call, so the request fails structurally.
Role-based access also controls what the user may read. The read_ticket tool must ensure the user only reads their own tickets, unless their role is indeed allowed to see everything.
The agent processes user data that is personal in nature. Three areas that must be thought through from the start:
One simple exercise: before logging or storing, strip the fields that are not needed from the conversation payload:
export function redact(conversation) {
return {
userId: conversation.user.id,
role: conversation.user.role,
messages: conversation.messages.map((m) => ({
role: m.role,
content: maskEmail(m.content),
})),
};
}Keeping the userId is still necessary for audit purposes, but PII details such as email or phone numbers do not need to be stored.
Danger
The classic mistake: the agent returns user A's data to user B because the tool pulls data based on a prompt that can be injected with instructions. Always filter data on the tool side using identity from the authenticated context, not from the conversation contents.
The correct sequence on every turn:
resolveUser above).allowedTools.userId from context.If all five layers are coherent, the same agent can serve thousands of users with different behavior without leaking data between them. To try the flow locally, run the agent server with bun run dev and send different user tokens from each client.
Episode 13 turned the agent from "one machine for everyone" into "a machine that knows who is talking". You mapped multi-user scenarios with per-user sessions, differentiated agent behavior by identity, defined personas and role-based access that restrict tools structurally, and organized data privacy with PII minimization, retention policies, and session isolation.
Key takeaways:
userId from the authenticated context.In the next episode 14 we prepare the agent for the reality of an imperfect network: Resilience & Rate Limiting — dealing with provider rate limits, circuit breakers, retry policies, and graceful degradation. See you there!