← Blog

How to Build a Multi-Tenant Claude App

2026-10-27 · 5 min read · SubToAPI Team

A multi-tenant Claude app is one codebase and one deployment that serves multiple separate customers, organizations, or workspaces — each with its own users, data, conversation history, and usage limits, but none of them able to see or affect the others. If you're building a SaaS product on top of Claude (a support tool, an internal copilot you're selling to other companies, a white-labeled chatbot), this is the architecture you need from day one, because retrofitting tenant isolation after launch is painful.

The short answer: you need a tenant resolution layer in front of every Claude request, a data model that scopes conversations and usage by tenant, and a way to attribute token consumption back to each tenant for billing and rate limiting. Everything else is detail. Below is how to structure each piece.

Define what "tenant" means in your app

Before writing any code, decide what a tenant actually represents:

This decision drives your database schema and your authorization checks. Most B2B SaaS products go organization-level with per-user roles inside.

Core data model

At minimum you need:

tenants        (id, name, plan, model_config, rate_limit)
users          (id, tenant_id, role)
conversations  (id, tenant_id, user_id, created_at)
messages       (id, conversation_id, role, content)
usage_events   (id, tenant_id, input_tokens, output_tokens, created_at)

The tenant_id foreign key on every table is the backbone of isolation. Every query that touches conversations or usage must filter by tenant_id, and that filter should come from server-side auth context — never from a client-supplied header or query param you haven't verified.

Resolve the tenant before touching Claude

Every incoming request needs a tenant resolution step that runs before any API call. Common strategies:

async function resolveTenant(req, res, next) {
  const tenantId = req.auth?.tenantId; // from verified JWT, not from client input
  if (!tenantId) return res.status(401).end();

  const tenant = await db.tenants.findById(tenantId);
  if (!tenant) return res.status(404).end();

  req.tenant = tenant;
  next();
}

Once you have req.tenant, every downstream call — model selection, system prompt, rate limit check, tool permissions — reads from that object instead of global config.

Keep conversation state scoped per tenant

Claude's API is stateless: you send the full message history with every request. That means isolation is entirely your responsibility at the storage layer. Always load and save conversation history using a composite key of tenant_id + conversation_id, and reject any request where the authenticated tenant doesn't match the conversation's owner.

app.post("/api/chat", resolveTenant, async (req, res) => {
  const { conversationId, message } = req.body;
  const convo = await db.conversations.find({
    id: conversationId,
    tenant_id: req.tenant.id, // scoping check
  });
  if (!convo) return res.status(403).end();

  const history = await db.messages.listByConversation(convo.id);
  const response = await fetch("https://api.subtoapi.app/v1/messages", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SUBTOAPI_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: req.tenant.model_config.model,
      system: req.tenant.model_config.system_prompt,
      messages: [...history, { role: "user", content: message }],
    }),
  });

  const data = await response.json();
  await db.messages.save(convo.id, "assistant", data.content);
  await db.usage_events.record(req.tenant.id, data.usage);
  res.json(data);
});

Note the usage_events.record call — this is the step most people skip until billing becomes a mess. Record token usage per tenant on every call, not at the end of the month from an estimate.

Per-tenant rate limits and quotas

If one tenant on a free or starter plan can exhaust the shared quota and degrade service for everyone else, your multi-tenant app isn't actually isolated — it's just logically separated data on a shared resource that fails together. Enforce limits per tenant before the Claude call goes out:

This is also where issuing separate application API keys per tenant pays off. If each tenant (or each customer-facing environment) gets its own sub_live_... key through SubToAPI, you get usage metadata and rate limiting scoped to that key automatically, instead of building all of the accounting logic yourself. You can create and manage those keys from the SubToAPI dashboard and route tenant traffic through the matching key.

Streaming and tool use across tenants

Streaming responses work the same way per tenant — the SSE connection is tied to the authenticated request, so isolation is already handled by your tenant resolution middleware. See /docs/streaming for the event format if you're piping responses to the browser.

Tool use needs an extra check: tool definitions and the functions they call often touch tenant-specific data (a CRM lookup, a database query). Make sure the tool execution layer also scopes by req.tenant.id — Claude only decides that a tool should run, your backend decides what it's allowed to touch. The /docs/tools reference covers the request/response shape for tool calls.

Billing and visibility

Once usage is recorded per tenant, map it to your pricing tiers and surface it back to each tenant (a simple "tokens used this month" widget goes a long way toward preventing support tickets). If you're running on SubToAPI, usage metadata comes back on every response from /docs/messages, so you don't need to compute token counts yourself — just store what's returned and sum it per tenant.

Checklist before launch

Start with /docs/quickstart if you're wiring up the API calls for the first time, and /pricing if you're deciding how to split API keys across plans as you add tenants.

FAQ

Do I need separate Claude API keys for each tenant? Not strictly — you can run everything through one key and track usage in your own database. Separate keys per tenant (or per plan tier) make rate limiting and billing easier because the provider tracks usage per key automatically, which scales better once you have more than a handful of tenants.

How do I prevent one tenant's data from leaking into another's conversation? Scope every database read and write by tenant_id, resolved server-side from an authenticated session — never trust a tenant ID sent from the client. Also check conversation ownership on every request, not just on creation.

What's the fastest way to add multi-tenancy to an existing single-user Claude app? Add a tenant_id column to your conversations and usage tables, insert a tenant resolution middleware before your existing API call, and scope every query that touches those tables. You can ship this incrementally without rewriting your prompt or streaming logic.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →