Dev.to AI πŸ€– Ai πŸ‘ 0 πŸ“– 7 min read

Architecture of a Multi-Tenant Private ChatGPT SaaS on LibreChat: Silo Tenants, Seat Billing and Token Caps

This post is AI-assisted. ChatGPT Business costs $20 per user per month on annual billing and $25 on monthly billing. Claude Team's standard seat is the same $20 and $25. Microsoft 365 Copilot Business is $25.20 on mont

Architecture of a Multi-Tenant Private ChatGPT SaaS on LibreChat: Silo Tenants, Seat Billing and Token Caps

This post is AI-assisted.

ChatGPT Business costs $20 per user per month on annual billing and $25 on monthly billing. Claude Team's standard seat is the same $20 and $25. Microsoft 365 Copilot Business is $25.20 on monthly billing, on top of a Microsoft 365 license. That is a price umbrella, and a focused product can live under it: a private, branded company ChatGPT, connected to each customer's own tools, sold at $12 a seat.

The open-source core already exists. LibreChat (MIT, about 46.6K GitHub stars) gives you the chat UI, agents, skills, MCP, multi-user auth, an admin panel and token balances. Shopify runs its internal AI on it, with thousands of custom agents and more than 30 internal MCP servers, according to ClickHouse's announcement when it acquired the project in November 2025.

What LibreChat does not give you is the SaaS around it: signup, provisioning, isolation between customers, seat billing and cost control. This post is the architecture for that layer.

Two founders sketching a multi-tenant private ChatGPT architecture: control plane, tenants, Claude and MCP connectors

Requirements

Functional

  • A customer signs up, picks a plan and gets acme.yourapp.com within minutes
  • Their staff sign in with the customer's own identity provider (Google Workspace, Microsoft Entra ID, Okta)
  • Claude as the model, MCP connectors to the customer's tools, shared agents per customer
  • Billing per seat, updated automatically as people join and leave

Non-functional

  • Hard isolation of chats, files and connector tokens between customers
  • Bounded model spend per user and per customer
  • Upgrades that can be rolled to one customer first
  • Target cost per seat of about $3 to keep a 75% gross margin at $12

Silo, not pool

LibreChat is designed as one deployment per organization: one librechat.yaml, one .env, one user base, one admin panel. You could try to bolt tenant IDs onto every collection, but you would be fighting the codebase on every upgrade.

So the data plane is one stack per customer (the silo model), and the shared parts live in a small control plane you write yourself.

Concern Silo (one stack per tenant) Pool (shared stack)
Data isolation Separate database per tenant Tenant ID on every query
Per-tenant config, branding, MCP servers Native: separate yaml and env Needs a custom config layer
Noisy neighbor Contained Shared blast radius
Upgrades Canary per tenant All at once
Infra cost per tenant Higher Lower
Fit with upstream LibreChat Clean Fork pressure

Start silo. When you have hundreds of small tenants, pack several tenant containers per host, but keep separate databases.

The architecture

                 Control plane (yours)
  +================================================+
  | Signup + plan    Tenant registry (Postgres)    |
  | Stripe webhooks  Provisioner   Seat sync job   |
  | Usage + alerts   Upgrade orchestrator          |
  +================================================+
           | creates / updates
           v
  Data plane: one stack per tenant (acme.yourapp.com)
  +================================================+
  | LibreChat API + web (pinned image tag)         |
  | MongoDB database: acme (users, chats, balance) |
  | Meilisearch index (chat search)                |
  | RAG API + pgvector (file search)               |
  | MCP servers for this tenant's tools            |
  +================================================+
           |                          |
   Anthropic API                Customer IdP (OpenID)
   (one Workspace per tenant,   Customer tools via MCP
    own key + spend limit)       (per-user OAuth)

Three decisions do most of the work:

  1. One Anthropic Workspace per tenant. The Anthropic Console lets you group API keys into workspaces, with a monthly spend limit and rate limits per workspace. Each tenant stack gets its own workspace-scoped key. Spend reporting per customer comes for free, and a runaway tenant hits its own ceiling, not yours.
  2. OpenID per tenant. Each tenant's .env points at the customer's identity provider. People leave the company, they lose access. No passwords for you to store.
  3. Per-user OAuth for MCP. LibreChat supports stdio, sse, websocket and streamable-http MCP servers, and with requiresOAuth: true each person connects their own account. The assistant can only see what that person can see in the underlying tool.

The tenant config template

The provisioner renders this per tenant. The values that change are the tenant name, the spend cap and the MCP servers.

version: 1.3.18

interface:
  customWelcome: 'Welcome to Acme AI. Ask about your clients, files and projects.'

balance:
  enabled: true
  startBalance: 6000000        # 1,000,000 credits = $1, so $6 of tokens
  autoRefillEnabled: true
  refillIntervalValue: 30
  refillIntervalUnit: 'days'
  refillAmount: 6000000
  refillMode: 'reset'          # replace the allowance each period, no rollover
  display: 'currency'

mcpServers:
  documents:
    type: streamable-http
    url: https://mcp.acme.yourapp.com/documents/mcp
    requiresOAuth: true
    timeout: 60000

And the branding in the tenant .env:

APP_TITLE="Acme AI"
CUSTOM_FOOTER="Acme AI. Private assistant for Acme staff."
ALLOW_REGISTRATION=false
ALLOW_SOCIAL_LOGIN=true
OPENID_ISSUER=https://login.acme-idp.example.com
OPENID_CLIENT_ID=from-the-customer
OPENID_BUTTON_LABEL="Sign in with Acme"
ANTHROPIC_API_KEY=key-from-the-acme-workspace

The refillMode: 'reset' line matters for a seat business. With the older add behavior, unused allowance rolls over and a quiet user can bank a large balance. Reset caps every person at the same monthly ceiling. Check that every replica runs a version that supports reset before you switch, as the example config warns.

Provisioning flow

// control-plane/provision.ts (sketch)
export async function provisionTenant(t: Tenant) {
  const ws = await createAnthropicWorkspace(t.slug, { monthlyLimitUsd: t.seats * 6 });
  await createMongoDatabase(t.slug);
  await writeConfig(t.slug, renderYaml(t), renderEnv(t, ws.apiKey));
  await deployStack(t.slug, { image: `librechat:${PINNED_TAG}` }); // ECS service or compose on a host
  await createDnsRecord(`${t.slug}.yourapp.com`);
  await waitForHealthy(`https://${t.slug}.yourapp.com/health`);
  await registry.markActive(t.id);
}

createAnthropicWorkspace is a placeholder for however you manage workspaces and keys; whether you do it by API or once by hand in the Console, keep the key in a secrets manager and inject it at deploy time.

Seat billing that follows reality

Bill on seats provisioned, sync once a day, and let Stripe prorate.

// control-plane/seat-sync.ts
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function syncSeats(t: Tenant) {
  const users = await tenantDb(t.slug).collection("users").countDocuments({});
  const seats = Math.max(users, t.minimumSeats);
  if (seats !== t.billedSeats) {
    await stripe.subscriptionItems.update(t.stripeSeatItemId, {
      quantity: seats,
      proration_behavior: "create_prorations",
    });
    await registry.setBilledSeats(t.id, seats);
  }
}

Stripe Billing costs 0.7% of billing volume on pay as you go, plus the standard US card fee of 2.9% plus 30 cents. On a $600 monthly invoice for 50 seats that is about $0.44 per seat.

Cost control in three fences

  1. Per user: LibreChat balances, as configured above. When a user's allowance is spent, their requests stop until the next reset.
  2. Per tenant: the Anthropic Workspace spend limit, set to roughly seats times the per-user cap.
  3. Per model: route by task. Claude Haiku 5.5 is $0.10 per million input tokens and $0.50 per million output tokens for prompts up to 100K tokens. Claude Sonnet 5.5 is $2 and $10. Make Haiku the default for quick questions and send agents and long documents to Sonnet.

The model mix is the biggest lever in the whole business. With 300 messages per seat per month, 4,000 input and 600 output tokens per message:

Mix Token cost per seat per month
100% Sonnet 5.5 $4.20
30% Sonnet, 70% Haiku $1.41
100% Haiku 5.5 $0.21

Add about $1.20 per seat for a $60 monthly hosting budget on a 50-seat stack (the t4g.medium that runs the app lists at about $24.53 a month on demand in us-east-1) and the $0.44 payments line. The routed mix lands at about $3.05 per seat, a 75% gross margin at $12. Prompt caching, which these numbers ignore, only improves it.

Upgrades without fear

  • Pin the LibreChat image tag per tenant in the registry.
  • Keep one internal tenant on the newest tag, then one friendly customer, then everyone.
  • Render configs from templates so a new config key is one change, not 50.
  • Run a smoke test after each tenant upgrade: login, one Haiku message, one Sonnet message, one MCP tool call.

What fails first

  • MCP OAuth tokens expire or get revoked. Make reconnecting a one-click path for users.
  • Long conversations grow context with every turn. Nudge people to start a new chat per task.
  • Slow MCP servers make the whole answer slow. Set per-server timeouts and alert on them.
  • Seat disputes. Show every customer admin the exact list of users being billed.

Build it with us

Axionry is an AI product development studio. We build private company assistants and multi-tenant AI products like this one, with Claude, MCP connectors, SSO and spend controls, and we ship them in weeks. The product page is at axionry.com/build/private-chatgpt, and you can book a call to talk through your version.

Sources

πŸ“° Read the original article on Dev.to AI

Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.