Skip to content

Multi-tenant apps

Run one app for users who work in more than one Tenant, or in more than one tenant application. Each Context (tenant plus application) has its own conversations, so each gets its own Chat client and its own token.

User scope: chat:access
  1. Take the tenant from the signed-in user. Use the tenant the user signed in to or picked in your own app, and the application id of the app they are using. Never hard-code a tenant, and never accept one from a URL without checking it against the user’s own memberships. Chat takes the tenant and application only from the request URL and checks them against the Identity Server.

  2. Create one client per context. ChatUserClient and ChatNotificationsClient take tenantId and applicationId in their options and cannot change them. Cache a pair per context, as in the sample. The push client is tenant-level and takes only tenantId.

  3. Fetch a token per context. A token issued to your application’s client works only in your application’s context. A call for any other context answers 403 client_context_mismatch. Pass the context to your token route, which exchanges with the backend client of that application. See Issue Chat tokens to your users.

  4. Switch by replacing the clients. When the user switches tenant or application, disconnect the live connection, drop the old clients and create new ones for the new context.

multi-tenant-chat.ts
import { ChatUserClient } from '@central-code-station/ccs-chat-user-client';
import { ChatNotificationsClient } from '@central-code-station/ccs-chat-notifications-client';
export interface ChatContextKey {
tenantId: string; // from the signed-in user's session, never typed in or guessed
applicationId: string;
}
const chatApiUrl = 'https://chat.centralcodestation.com/api/v1';
const clients = new Map<string, { chat: ChatUserClient; live: ChatNotificationsClient }>();
// Your route returns a Chat token for this context (see "Issue Chat tokens to your users").
declare function fetchChatToken(context: ChatContextKey): Promise<string>;
// One pair of clients per context. Switching tenant or application means a new pair.
export function chatFor(context: ChatContextKey) {
const key = `${context.tenantId}/${context.applicationId}`;
let entry = clients.get(key);
if (!entry) {
const options = { chatApiUrl, ...context, getAccessToken: () => fetchChatToken(context) };
entry = { chat: new ChatUserClient(options), live: new ChatNotificationsClient(options) };
clients.set(key, entry);
}
return entry;
}
// Leaving a context (switching workspace, signing out): stop its live connection and drop it.
export function leaveContext(context: ChatContextKey): void {
const key = `${context.tenantId}/${context.applicationId}`;
clients.get(key)?.live.disconnect();
clients.delete(key);
}
  • A user sees data only of the tenant in the URL, and only if they are an active member of it. Otherwise Chat answers 403 not_tenant_member or no_application_access.
  • A conversation of another tenant or application is a 404 not_found. Chat does not reveal that it exists.
  • Presence and web push devices are tenant-level: they use tenantId and no application.

Sign in as a user who belongs to two tenants. In each tenant, list conversations: you see only that tenant’s conversations. Switch tenants in the UI and check that the live connection of the first tenant closes and that no event of the first tenant reaches the second.

Symptom Cause and fix
403 client_context_mismatch The token was issued to a client of another application than the one in the URL. Request the token for the context you call.
403 not_tenant_member The user is not an active member of that tenant. Check their membership in the Identity Server.
404 application_not_found The application does not exist in that tenant, or Chat is not enabled for it.
Conversations of the previous tenant stay on screen The old client is still in use. Replace it when the context changes.
Live events keep arriving for the old tenant The old ChatNotificationsClient was not disconnected.