Building a Multi-Tenant SaaS
Your product runs in front of the server; your customers never see it. Every credential here resolves to an OWNER, and storage is addressed by owner on every surface, so the question a SaaS team must answer is not "how do I isolate customers" but "what is one customer, in credentials". One Server, Many Teams is the tenancy architecture; this page maps customers onto it, scripts the provisioning, shows the isolation test, says what the server meters and what your application must add, and ends with the offboarding sequence.
1Three shapes for "one customer"#
| Shape | The credential | What isolates by itself | What your application must do | Fits when |
|---|---|---|---|---|
| A. One key per customer | An API key minted per customer (and per environment), named for it | Everything an owner holds: uploaded files, vector stores, stored responses, agent memories, jobs; Search reach through the customer's own tenant grants; the request trail attributes every call to the customer | Store the token in your vault beside the customer record; provision and revoke with the customer's lifecycle (section 2) | Customers are organizations with contracts, audits and offboarding obligations |
| B. One key per service | One key for your backend, whatever customer it acts for | Nothing between customers: one owner holds every file, store, memory and job | Name every object per customer and never mix them: one vector store per customer, memory ids per customer (memory: "cust-<id>"), one Search tenant per customer with the service key granted to each tenant and the tenant chosen per request; put the customer id in X-Request-Id so the trail can be read per customer |
You already run a tenancy layer and want the engine alone |
| C. Provider tokens as owners | Your identity provider's JWT on every call, with a claim naming the customer or the user (Single Sign-On section 2) | The claim's value, prefixed oidc:, is the owner id: files, vector stores, stored responses and memories separate per claim value with no key management at all |
Nothing for those surfaces; but provider tokens hold no Search grants, so Search is off the table for this shape | End users or organizations authenticate with your IdP and the feature is chat, Responses and file grounding, without Search |
Two rules decide between them. A metadata filter is never a boundary: it is supplied by the caller, so it separates nothing (Access Model section 1). And a tenant is the smallest unit you will ever grant or revoke independently, so a customer you may have to offboard on its own is a tenant of its own, and in shape A a key of its own.
2Provisioning a customer, scripted#
Keys are minted by operators, from the console or the admin API; the admin API takes the same
session the console uses, so provisioning is three calls from your onboarding pipeline. Use a
dedicated operator account in the Admin role for the pipeline (Operator Accounts
section 2); the sign-in lands in the security trail like every other, and if the account carries a
second factor the code goes in the body as totpCode.
# 1. An operator session (the console's own header, X-Admin-Password, carries it)
SESSION=$(curl -sf -X POST "$LMKIT_URL/lmkit/v1/admin/login" -H 'Content-Type: application/json' \
-d '{"username":"provisioning","password":"'"$PROVISIONING_PASSWORD"'"}' | jq -r .token)
# 2. The customer's key: named for the customer, expiring, reaching the one cluster you serve them from
curl -sf -X POST "$LMKIT_URL/lmkit/v1/admin/apikeys" -H "X-Admin-Password: $SESSION" -H 'Content-Type: application/json' \
-d '{"name":"acme-prod","scope":"full","expiresInDays":365,"clusterGrants":["main"]}'
{ "id": "k_7f3a...", "name": "acme-prod", "token": "lmk_...", "createdUtc": "2026-09-09T10:00:00Z", "scope": "Full", "expiresUtc": "2027-09-09T10:00:00Z" }
The token appears in this response and never again: the store keeps a hash. Put it in the vault
under the customer record. Then, WITH THE CUSTOMER'S KEY, create the customer's Search tenant:
the key that creates a tenant is granted it, so no second operator call is needed.
curl -sf -X POST "$LMKIT_URL/lmkit/v1/search/tenants" -H "Authorization: Bearer $CUSTOMER_KEY" -H 'Content-Type: application/json' \
-d '{"cluster_id":"main","display_name":"Acme","enable_semantic_search":true}'
{ "id": "3f2b9c4e-...", "display_name": "Acme", "embedding_model": "qwen3-embedding:0.6b", "enable_full_text_search": true, "enable_semantic_search": true }
Collections follow with POST /lmkit/v1/search/collections under that tenant, one per dataset
(Clusters, Tenants, Collections places datasets; Indexing
Well fills them). The tenant carries its own embedding model and search
modes, so a later change of the server's default embedding slot never re-embeds a customer.
Rotation is POST /lmkit/v1/admin/apikeys/{id}/regenerate: a new token under the same key
identity, so the customer's grants, files and stores stay; the old token dies at once
(Keys and Authentication section 5). Rotate on your schedule and on any
suspicion, and keep one key per customer per environment, never one key shared by production
and staging.
3Isolation you can prove#
Ship the isolation test with your integration tests; it is four requests. With two keys, A and B:
STORE=$(curl -sf -X POST "$LMKIT_URL/v1/vector_stores" -H "Authorization: Bearer $KEY_A" -H 'Content-Type: application/json' -d '{"name":"acme-contracts"}' | jq -r .id)
curl -s -o /dev/null -w '%{http_code}\n' "$LMKIT_URL/v1/vector_stores/$STORE" -H "Authorization: Bearer $KEY_A" # 200
curl -s -o /dev/null -w '%{http_code}\n' "$LMKIT_URL/v1/vector_stores/$STORE" -H "Authorization: Bearer $KEY_B" # 404
curl -sf "$LMKIT_URL/lmkit/v1/search/collections" -H "Authorization: Bearer $KEY_B" # lists only what B reaches
The 404 is the contract: an object another owner holds does not exist for you, on every
dialect and on every node of a farm. The same test with previous_response_id (a chain cannot
cross owners), a file_id, a job_id and a tenant id gives the same answer. In shape B the test
is on your own layer instead: two customers of your product, the same probe, no leak.
4Metering and attribution#
What the server records, per request, on the node that served it: the timestamp, method, route,
status, duration, the key's id and name, the client address, the request id, request and
response sizes. It is the Requests page, filterable by key and by request id,
and GET /lmkit/v1/admin/requests/export?apiKeyId=<id> gives the same rows as CSV for one
customer. Per-key use counts and last-used timestamps sit beside each key in
Access.
What the server does NOT aggregate: tokens per key. Tokens are in every response as usage
(prompt_tokens, completion_tokens, total_tokens; a reasoning breakdown on the Responses
API; the final chunk of a stream when the request sets stream_options.include_usage). The
authoritative token ledger is therefore your application recording usage per customer as the
response arrives; the trail is the audit that the ledger reconciles against. Prometheus carries
request counts and durations tagged by route, method and status, never by key
(Observability section 4), so it sizes the fleet and does not bill.
On a farm the trail is each node's own history: export from every node, or let the ledger be the source and use the trail per node for disputes.
5Fair use: what the server enforces, and what is yours#
| Enforced by the server | How |
|---|---|
| In-flight task ceiling per key | MaxJobsPerOwner (100 by default): 429 too_many_inflight_jobs with Retry-After: 5; fleet-wide on a farm, each node admitting its share (Errors, Retries, and Jobs section 3) |
| Compute under contention | The saturation policy, queue or 503 with Retry-After, for everyone alike (Inference Capacity section 2) |
| Upload lifetime | MaxFileRetention (30 minutes by default): uploads are transient input, your application keeps the source |
| Reach | Scope (full or read-only), tool grants, cluster and tenant grants per key |
| Yours to build | Where |
|---|---|
| Requests per minute or tokens per month, per customer | Your API gateway or application; the server has no per-key rate limit or quota, by design: the saturation policy protects the engine, fairness is a product decision |
| Plan limits (models allowed, features enabled) | Your application, by choosing model and features per customer; a key cannot be restricted to a model |
| Throughput isolation between customers | A sizing decision, not a setting: owners isolate data, not slots. A customer who must not share compute gets a separate server or fleet (Reference Architectures) |
6Offboarding a customer#
Delete data with the customer's key first, then revoke the key: a revoked key can no longer delete its own objects, and the console's Search section is the fallback for a tenant whose key is already gone.
- Stop routing the customer in your application.
- Search:
DELETE /lmkit/v1/search/clusters/{clusterId}/tenants/{tenantId}removes the tenant with its collections and indexed content, once, in the engine every node reaches. - Vector stores and files: list them (
GET /v1/vector_stores,GET /v1/files) and delete each (DELETE /v1/vector_stores/{id},DELETE /v1/files/{id}); files you do not delete expire withMaxFileRetention. - Stored responses:
DELETE /v1/responses/{id}for the ids your application chained (Responses and Vector Stores section 1). - Memories: with an operator session,
DELETE /lmkit/v1/admin/memory/stores/{store}/keys/{memoryId}for each memory id the customer's conversations used; the ids are the ones your application armed (Agent Memory section 4). A forget reaches every node. - Revoke the key (
POST /lmkit/v1/admin/apikeys/{id}/revoke), which keeps its history for the audit, or delete it (DELETE /lmkit/v1/admin/apikeys/{id}). - Job records expire an hour after the job ends; nothing else of the customer's remains (Where Data Lives section 4 is the disposal inventory).
In shape C the same steps run with the customer's own token for steps 3 and 4, and there is no key to revoke: the claim stops being issued by your IdP.
7Stated plainly#
- Decide what one customer is in credentials: a key per customer (the full boundary, Search included), a key per service (your layer separates), or a provider-token owner (no keys, no Search).
- Provisioning is three calls: an operator session, a named expiring key with its cluster grant, and a tenant the customer's key creates and is granted.
- Isolation is provable with two keys and a
404; ship that test. - The server attributes every request to its key and returns
usageon every response; your application owns the token ledger and the quotas. - Offboard in order: delete data with the customer's key, then revoke; the tenant, the stores, the files, the chained responses and the memory ids are the named paths.