LM-Kit OneDocs2026.9.7lm-kit.comEULA
Access

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.

  1. Stop routing the customer in your application.
  2. 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.
  3. 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 with MaxFileRetention.
  4. Stored responses: DELETE /v1/responses/{id} for the ids your application chained (Responses and Vector Stores section 1).
  5. 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.
  6. 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}).
  7. 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 usage on 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.