Skip to content

Multi-tenant deployments

One KDBL Context Lake (K-Lake) deployment can serve many organisations — or many business units — as fully separate tenants. Each tenant has its own sources, users, tokens and index rows, and from 0.1.36 each tenant also has its own AI endpoint, its own sign-in issuer, optionally its own identity provider, and optionally its own token-signing keys.

This page is the step-by-step for a service provider or MSP standing up a shared deployment. It assumes the deployment already has a public MCP address and a working identity provider — the Connect your AI and OAuth and identity providers pages get you there. Each of the steps below takes a few minutes per tenant.

Running the deployment for many customers — partners and platform operators, suspending and deleting tenants, quotas, fair sharing, branding and custom hostnames — is covered in Hosting many customers.


What "isolated" means here

Boundary How it is kept
Data Every source, file, user and token belongs to exactly one tenant; a request only ever sees its own tenant's rows.
AI endpoint Each tenant has its own MCP URL and its own sign-in issuer. A token minted for one tenant is refused at every other tenant's endpoint.
Sign-in A tenant can federate to its own identity provider, or share one with other tenants and be told apart by audience or an organisation claim. Its administrators see and change only their own; no tenant can point another at a directory it controls.
Keys A tenant can sign its own tokens. Tokens for that endpoint are then verified against that tenant's keys alone.
Clients An AI client registered under a tenant's endpoint belongs to that tenant and works nowhere else.
Suspension Suspending a tenant refuses its users on every path — console, SSO, API tokens, MCP and download links — and holds its ingest, while keeping its data. Resume reverses it.
Deletion Deleting a tenant ends its endpoint, every token minted for it and every client bound to it, in the same moment. Its data is then removed in the background, irreversibly; see Tenant lifecycle.

Tenant names (slugs) become path segments and issuer strings, so they are restricted to lowercase letters, digits and hyphens at creation.

One address, many tenants

Every tenant is served from the same public address. The first tenant connected answers on the bare endpoint — the shape every earlier deployment was connected with, kept so nobody has to reconnect. Every other tenant gets its own endpoint under /t/<tenant>/:

Endpoint Sign-in issuer Discovery documents
First tenant https://<host>/mcp https://<host> /.well-known/oauth-protected-resource/mcp
/.well-known/oauth-authorization-server
Tenant acme https://<host>/t/acme/mcp https://<host>/t/acme /.well-known/oauth-protected-resource/t/acme/mcp
/.well-known/oauth-authorization-server/t/acme

A distinct issuer per tenant is what lets K-Lake resolve a token to its tenant: the token names exactly one. K-Lake's built-in sign-in server presents /t/<tenant>/authorize, /t/<tenant>/token and /t/<tenant>/register for each tenant, and a code or refresh token can only be redeemed at the token endpoint that minted it.

Nothing changes for a single-tenant deployment

If you only ever connect one tenant, it holds the bare /mcp endpoint and everything on this page is optional. Existing connections keep working through an upgrade.

Step 1 — Connect each tenant to MCP

Sign in as a cluster administrator, open AI access (MCP) (or press ⌘K / Ctrl+K and type set up AI access), and on the Setup tab expand step 3. Choose which tenant it serves. Every tenant is listed with its state:

Step 3, "Choose which tenant it serves", of Set up AI access: the acme tenant is Connected at the bare endpoint address with a Take off MCP button; the other tenants are Local only with Connect and "Move bare endpoint here" buttons.

  • Connect — gives the tenant an endpoint. While the bare endpoint is free, the tenant takes it; otherwise the tenant is connected at its own endpoint, …/t/<tenant>/mcp, and the row says Connected (own endpoint) with the URL to give that tenant's users.
  • Move bare endpoint here — hands the bare /mcp endpoint from its current holder to this tenant. The previous holder is moved to its own endpoint in the same operation, so it stays reachable, but its users must reconnect at the new URL. It confirms first, naming both tenants.
  • Take off MCP — releases the tenant's endpoint. Its tokens stop validating immediately.

Nothing is typed: the issuer and audience are derived from the endpoint address set in step 2 and merged into the tenant's existing configuration, so directory-correlation settings survive.

# Bare endpoint if it is free, otherwise the tenant's own
curl -X PUT "$KDBL_URL/api/mcp/tenants/acme" \
  -H "Authorization: Bearer $KDBL_ADMIN_TOKEN" \
  -H "Content-Type: application/json" -d '{}'

# Insist on the tenant's own endpoint, /t/acme/mcp
curl -X PUT "$KDBL_URL/api/mcp/tenants/acme" \
  -H "Authorization: Bearer $KDBL_ADMIN_TOKEN" \
  -H "Content-Type: application/json" -d '{"scoped": true}'

# Insist on the bare endpoint; 409 while another tenant holds it
# unless take_over moves it
curl -X PUT "$KDBL_URL/api/mcp/tenants/acme" \
  -H "Authorization: Bearer $KDBL_ADMIN_TOKEN" \
  -H "Content-Type: application/json" -d '{"scoped": false, "take_over": true}'

GET /api/mcp/tenants lists every tenant with its endpoint, whether it is scoped, and which sign-in leg it uses (federation: tenant | deployment | none). DELETE /api/mcp/tenants/acme takes it off MCP. Cluster-admin only.

kdbl-control --api-url "$KDBL_URL" --api-token "$KDBL_ADMIN_TOKEN" \
  mcp tenants                                 # every tenant and its endpoint
kdbl-control --api-url "$KDBL_URL" --api-token "$KDBL_ADMIN_TOKEN" \
  mcp tenant-apply --tenant acme              # connect
kdbl-control --api-url "$KDBL_URL" --api-token "$KDBL_ADMIN_TOKEN" \
  mcp tenant-clear --tenant acme              # take off MCP

Give each tenant's users their URL. AI access (MCP) › Setup shows it per tenant (a tenant's own admin sees theirs under Settings › AI assistants › Endpoint › MCP endpoint), and it is the Base URL for every client on MCP clients.

Step 2 — Give a tenant its own identity provider (optional)

By default every tenant signs in through the deployment's identity provider — the one on AI access (MCP) › Setup › Let people sign in. A tenant that brings its own directory sets it on the Identity provider for MCP card:

  • as a cluster administrator, on the tenant's page under Tenants;
  • as that tenant's own administrator, on the Sign-in card under Settings › AI assistants (or press ⌘K / Ctrl+K and type identity provider for MCP) — self-service, so a customer can connect their directory without a ticket.

The "Identity provider for MCP" card under Settings › AI assistants, badged "deployment default", with fields for issuer, client ID, client secret, scopes and groups claim.

Field Value
Issuer The tenant's IdP issuer, exactly as it appears in its tokens — for Entra https://login.microsoftonline.com/<directory-id>/v2.0.
Client ID The app registration K-Lake signs this tenant's users in through. Register it in the tenant's IdP the same way as in Step 1 of the OAuth guide.
Client secret Required the first time. Sealed before it is stored, never shown or returned; leave the field empty on later saves to keep it.
Scopes Default openid profile email.
Groups claim Name of the groups claim in that IdP's token. Default groups.

The redirect URI to register at the tenant's IdP is the same for every tenant: https://<host>/oauth/callback. There is no per-tenant callback.

Sign-ins arriving under that tenant's endpoint then go to that tenant's directory, validated with that tenant's own TLS trust anchors. Click Save identity provider to save it; the badge changes from deployment default to own IdP. Use the deployment's provider instead clears it. Through the API: GET/PUT/DELETE /api/mcp/tenants/:slug/federation (issuer, client_id, write-only client_secret, optional scopes and groups_claim); a tenant's own administrator may call it for their own tenant.

Several tenants on one identity provider

A tenant does not need a directory of its own. An MSP that runs one identity provider — one realm, one directory — for many customers can put several tenants on the same issuer. K-Lake then tells the tenants apart by the token's audience (give each tenant its own client in the identity provider and set that tenant's audience) or, if that is still ambiguous, by an organisation claim named in the tenant's tenant_claim. A token that matches none of them is refused. Set a shared issuer on the tenant's Sign-in (SSO) card or through the API; the rules are described in Hosting many customers.

Each tenant still has its own K-Lake sign-in issuer and endpoint, as in the table above, so a token minted for one tenant's endpoint is still refused at every other.

Step 3 — Give a tenant its own signing keys (optional)

Tokens for every tenant are signed with the deployment's keys unless a tenant has keys of its own. Generate them on the Signing keys for MCP card: on the tenant's page under Tenants for a cluster administrator, or under on the Sign-in card under Settings › AI assistants for the tenant's own administrator. The badge reads deployment keys until the tenant has its own:

The "Signing keys for MCP" card under Settings › AI assistants, badged "deployment keys", with its Advanced section open: the tenant's tokens are signed by the deployment's keys until it generates its own, and a Generate this tenant's own key button.

  • Generate this tenant's own key (under Advanced) — tokens for this tenant's endpoint are signed by it and verified against it alone, published at https://<host>/t/<tenant>/.well-known/jwks.json. Sessions signed by the deployment's keys end at that moment; connected assistants carry on through their refresh tokens rather than prompting everyone to sign in again.
  • Generate a new key (rotate) — the newest key signs; older ones stay published so tokens they signed keep working until they expire.
  • Remove a key — tokens it signed no longer verify. Removing the last key returns the tenant to the deployment's keys.

Through the API: GET/POST /api/mcp/tenants/:slug/keys, DELETE /api/mcp/tenants/:slug/keys/:kid.

When to bother: a customer contract that requires their tokens to be verifiable against material only they can revoke, or a wish to rotate one tenant without touching the others. For most deployments the shared keys are fine.

Step 4 — Register clients per tenant

Assistants that register themselves (Claude, ChatGPT, Cursor) do so at the tenant's endpoint and need nothing here. Hosts that ask for a client ID and secret — Microsoft 365 Copilot, Copilot Studio, most enterprise agent platforms — are registered under Settings › AI assistants › Sign-in › MCP clients › Register a client (or press ⌘K / Ctrl+K and type MCP clients) with the Tenant endpoint picker set to the tenant. Register a client opens the form under the list of registered clients, and Use Microsoft 365 Copilot preset fills it for Copilot:

The Register a client form: a Use Microsoft 365 Copilot preset button, name, redirect URIs, a Tenant endpoint dropdown showing "This tenant (acme) — /t/acme/mcp", a ticked Confidential client (issue a secret) checkbox, an "Allow a plain-http redirect URI" checkbox and a Register client button.

The connector values that come back are for that tenant's endpoint — https://<host>/t/<tenant>/authorize, …/token — and the client is refused at every other tenant. The same from the CLI:

kdbl-control --api-url "$KDBL_URL" --api-token "$KDBL_ADMIN_TOKEN" \
  mcp client create --preset copilot --tenant acme

Step 5 — Sign the console in through the same server (optional)

A tenant on MCP can use K-Lake's own sign-in server for the web console too, so its users sign in to the console with the same directory and the same roles. That is a two-value change on the tenant's Sign-in (SSO) card and is walked through on Console sign-in and roles.

Step 6 — Verify

Each tenant advertises itself, unauthenticated, so you can check from anywhere:

HOST=https://mcp.example.com

# The tenant's own sign-in server and resource
curl -s $HOST/.well-known/oauth-authorization-server/t/acme | jq -r .issuer
# → https://mcp.example.com/t/acme
curl -s $HOST/.well-known/oauth-protected-resource/t/acme/mcp | jq -r .resource
# → https://mcp.example.com/t/acme/mcp

# A tokenless call is challenged, and the challenge points at the tenant's document
curl -si -X POST $HOST/t/acme/mcp | grep -i www-authenticate

# The tenant's keys, if it has its own
curl -s $HOST/t/acme/.well-known/jwks.json | jq '.keys | length'

Then connect an assistant at the tenant's URL and watch the sign-in land in AI access (MCP) › Tokens and in kdbl-control mcp audit, as on the OAuth guide.

Troubleshooting

Symptom Cause Fix
Connecting a second tenant returns 409 You asked for the bare endpoint (scoped: false) while another tenant holds it Use the console's Connect (own endpoint), send {} or {"scoped": true}, or take_over deliberately
A user's assistant connects but every call is 401 They connected at another tenant's URL, or a token from the bare endpoint was presented at /t/<tenant>/mcp Give them their tenant's URL from AI access (MCP) › Setup and reconnect
The tenant's new IdP is ignored — sign-in still goes to the deployment's Users are connecting at the bare endpoint, which federates to the deployment default The tenant must be Connected (own endpoint); check federation: tenant in GET /api/mcp/tenants
The IdP rejects the sign-in Redirect URI not registered Register https://<host>/oauth/callback — one URI for all tenants
Everyone in one tenant was signed out at once Its first own signing key was generated, or a key was removed Expected. Assistants resume through refresh tokens; anyone without one signs in again
A tenant administrator cannot see another tenant's provider Working as designed Each tenant's administrators manage only their own; cluster administrators see all
Saving an issuer on the Sign-in (SSO) card is refused (409) because another tenant uses it A tenant on that issuer could not be told apart from another Do what the message says: give each tenant its own API audience, or tenants sharing an audience a tenant_claim on the same claim with different values — see Hosting many customers
Sign-in fails for tenants that share an identity provider A token matches more than one of them, or none Give each tenant its own client and audience, or set a tenant_claim that its users' tokens carry
Every sign-in to one tenant returns 403 tenant suspended or tenant is being deleted The tenant was suspended or is being deleted Resume it, or see Tenant lifecycle

See also