Multi-tenant Patterns

Purpose and Scope

This guide explains how to compose eve's multi-tenant building blocks into a deployable architecture. In eve, a tenant is an application-owned boundary such as a customer organization, workspace, or account. The framework does not replace your membership database, credential vault, policy engine, or memory store. Instead, it carries verified inbound identity into each turn so your code can derive the tenant scope before tools, connections, approvals, and memory logic run. That division is the central pattern: eve transports authenticated context; your application decides what that context means.

Sources: docs/patterns/multi-tenant-auth.md

The repository documentation describes multi-tenant outbound auth as a pattern where authored tools and connections use the active turn context to choose tenant-scoped credentials. Tool executors receive ctx directly, OpenAPI and MCP connection auth may be async functions of ctx, and connection headers may be async maps or individual async values. The important operational guarantee is that the model does not see, select, or reason about credentials. It asks for work; your runtime code converts verified route identity into the right outbound secret.

Sources: docs/patterns/multi-tenant-auth.md

A production multi-tenant agent usually needs three concerns to line up. First, inbound route auth must prove who is calling and which tenant they are acting within. Second, outbound auth must select credentials for external systems without taking tenant IDs from prompts or tool inputs. Third, approvals and memory must use the same verified scope, so human-in-the-loop policy and long-term context cannot drift away from the caller's tenant. Treat these as one architecture rather than separate features, because an agent turn may use all of them in one workflow.

Relevant Source Files

  • docs/patterns/multi-tenant-auth.md - Defines the first-party multi-tenant outbound auth pattern, including tenant derivation from route auth, custom channel authentication, non-interactive connection auth, async headers, OpenAPI and MCP credential selection, and authored tool execution with tenant-scoped credentials.

Establish Tenant Scope at the Route Boundary

Start by configuring route authentication so the accepted principal includes a string tenantId attribute. The docs centralize this check in a helper named requireTenantCaller(ctx), which reads ctx.session.auth.current, verifies that the current principal is a user, verifies that attributes.tenantId is a string, and returns { tenantId, userId }. This helper is intentionally small, but it is the anchor for the whole pattern: every tenant-sensitive operation should call it or an equivalent helper before touching application data or credentials.

Sources: docs/patterns/multi-tenant-auth.md

The tenant must come from verified route auth, not from a prompt, tool argument, client context field, or remote API response. That rule protects against prompt injection and confused-deputy failures. A model can ask to access a tenant, and a user can mention a tenant in text, but neither should be trusted as authorization evidence. If your application lets a user switch between organizations, authenticate the selected organization on every session creation or continuation request, then stamp that selected tenantId onto the current turn's session auth context.

Sources: docs/patterns/multi-tenant-auth.md

The same route-auth boundary also gives you a clean place to adapt existing application auth. The docs show an agent/channels/eve.ts channel that imports eveChannel, localDev, and AuthFn, then defines a tenantAppAuth() function. That function calls application code such as verifyAgentCaller(request), returns null when the caller is not authenticated, and otherwise returns a session principal with authenticator, issuer, principalId, principalType, subject, and attributes containing tenantId and roles. This is deliberately application code: eve expects your app to validate API keys, JWTs, or sessions before a run starts.

Sources: docs/patterns/multi-tenant-auth.md

import type { SessionContext } from "eve/context";
 
export function requireTenantCaller(ctx: SessionContext): {
  tenantId: string;
  userId: string;
} {
  const caller = ctx.session.auth.current;
  const tenantId = caller?.attributes.tenantId;
 
  if (caller?.principalType !== "user" || typeof tenantId !== "string") {
    throw new Error("An authenticated tenant user is required.");
  }
 
  return { tenantId, userId: caller.principalId };
}

Outbound Auth for Tools, OpenAPI, and MCP

Once the tenant scope is established, outbound auth should be non-interactive and deterministic. The repository docs call out a reusable helper for Bearer tokens or tenant-scoped JWTs. A non-interactive connection authorization definition can require principalType: "user", which tells eve to require an authenticated route principal, key the step-local token cache by that user, and pass the projected principal into token acquisition. The helper can then look up tenant credentials from your store and return a token without exposing credential choice to the model.

Sources: docs/patterns/multi-tenant-auth.md

This pattern applies equally to OpenAPI and MCP connections. OpenAPI auth may be an async function of the active context, MCP auth may also resolve from context, and connection headers may be generated asynchronously. The practical implication is that connection definitions can stay shared across tenants while the credential material is selected per turn. That is especially important for software-as-a-service products where one agent deployment serves many customer organizations and each organization has separate API keys, warehouse tokens, or service accounts.

Sources: docs/patterns/multi-tenant-auth.md

Authored tools follow the same rule but receive the context more directly. A tool executor can call requireTenantCaller(ctx), use the returned tenantId and userId to fetch an API key, and then call your backend or a third-party service. Do not add tenantId as a trusted model-controlled input just because the downstream API requires one. If a tool input includes a tenant field for compatibility, validate it against the authenticated tenant and reject mismatches before making the outbound call.

Sources: docs/patterns/multi-tenant-auth.md

SurfaceTenant sourceCredential selectionRecommended boundary
Authored toolctx.session.auth.currentTool executor reads application credential storerequireTenantCaller(ctx) before execution
OpenAPI connectionActive connection contextAsync auth function or async headersNon-interactive auth helper keyed by authenticated user
MCP connectionActive connection contextAsync auth functionNon-interactive auth helper keyed by authenticated user
Channel routeHTTP requestApplication verifier returns session autheveChannel({ auth: [...] })

Approval and Human-in-the-loop Policy

Multi-tenant approvals extend the same scope into policy decisions. The official approvals pattern describes eve's approval field as an async policy hook that receives the active session, the qualified tool name, tool input, and previously approved tools. That is enough information to ask your application whether a tenant should allow, deny, or require human confirmation for an authored tool, OpenAPI operation, or MCP tool. The policy storage still belongs to your application; it might be PostgreSQL, a policy service, an authorization engine, or durable KV configuration.

The safest approval adapter compares the current caller with the initiating caller and confirms that both are pinned to the same tenant user before consulting policy. That protects continued sessions and delegated operations from accidentally switching authorization context. After the tenant is established, the adapter can inspect the tool name and input, check tenant policy, and return an approval status such as allowed, denied, or human confirmation required. Reuse the adapter across authored tools and connection tools so policy behavior does not vary by integration surface.

Approvals are not a replacement for route auth or outbound auth. They answer a different question: even if the caller is authenticated and a credential exists, should this operation proceed automatically? For example, a tenant may allow read-only warehouse queries without confirmation but require approval before spending money, exporting customer data, or modifying a production issue tracker. Keep approval decisions tied to authenticated tenant context and previously approved tools, not to model assertions about what the user wants.

Tenant-scoped Memory

The official memory pattern deliberately avoids a built-in tenant-aware memory subsystem. Instead, it composes existing eve primitives: route auth places tenant and user information on the session, dynamic instructions load memories before a turn, and ordinary tools write or delete memories in your application store. That design keeps storage decisions outside eve, so teams can use PostgreSQL, durable KV, or vector databases as long as every read and write is scoped by tenant and user.

For memory, the most important rule is the same as outbound auth: never accept tenant or user identity from the model. Derive the memory scope from ctx.session.auth.current or, for conversations permanently owned by the creator, from ctx.session.auth.initiator with channel-boundary enforcement. Dynamic instructions should resolve on turn.started so later turns in the same session see memories written by earlier turns. Memory tools such as remember, forget, and list should call the same tenant helper before touching the store.

A deployable memory design should separate model-visible memory content from authorization metadata. The model may see a summarized preference or durable note, but it should not receive raw credential identifiers, policy internals, or cross-tenant keys. Store tenant ID, user ID, provenance, timestamps, and deletion state in your application database. Then render only the memories that belong to the authenticated scope into dynamic instructions. This gives the model helpful continuity while preserving tenant isolation at the storage and retrieval layers.

Implementation Checklist

Use one route-auth adapter per application identity system and put it in the channel auth walk before development fallbacks. The adapter should validate the incoming request, confirm tenant membership, return a stable principalId, include an issuer when IDs can originate from more than one identity system, and place routing facts such as tenantId in attributes. The repository pattern explicitly distinguishes this from connection OAuth: the user is already authenticated to your application, and eve uses that verified principal to pick the correct outbound credential.

Sources: docs/patterns/multi-tenant-auth.md

Next, create shared tenant helpers instead of duplicating checks across tools and connections. One helper should accept SessionContext for authored tools and dynamic instructions. A second helper may accept a ConnectionPrincipal for non-interactive connection auth. Both helpers should enforce a user principal and a string tenant ID. Then build thin adapters for your credential store, approval policy store, and memory store. Each adapter should require tenant scope as an input rather than deriving it from untrusted model parameters.

Sources: docs/patterns/multi-tenant-auth.md

Finally, test the negative paths as carefully as the successful paths. Exercise missing auth, wrong principal type, missing tenant attributes, tenant mismatch in tool input, unauthorized selected organizations, denied approval policy, and missing tenant credentials. In local development, localDev() can keep the channel usable, but production browser traffic should be accepted only by authenticators that prove the caller. The core architecture is simple because every tenant-sensitive operation starts from the same verified session context and rejects anything that tries to bypass it.

Sources: docs/patterns/multi-tenant-auth.md

Next Steps

Read the Auth and Route Protection guide before implementing this pattern in a production app, because route auth is the source of the tenant scope used everywhere else. Then apply the outbound auth pattern to authored tools, OpenAPI connections, and MCP connections. After credentials are isolated, add the approvals adapter for tenant policy and the memory pattern for tenant-scoped long-term context. The result is an eve agent that can serve many organizations from one deployment without asking the model to choose tenants, credentials, or authorization policy.