---
title: Multi-Tenant Agent
description: Build a SaaS where each customer runs their own commerce agent with their own providers and credentials. Sessions are scoped per tenant — isolated by design, metered per tenant for billing.
---

<ActionBar githubPath="content/docs/cookbooks/multi-tenant.mdx" markdownPath="/api/docs-md/cookbooks/multi-tenant" />

<MetaStrip items={[
  { label: "TIME", value: "~15 min" },
  { label: "USE CASE", value: (<><ServerChip name="SaaS" accent /><ServerChip name="Marketplaces" /><ServerChip name="Agency" /></>) },
  { label: "STACK", value: (<><ServerChip name="Next.js" /><ServerChip name="OpenAI" /></>) },
]} />

Build a SaaS where each customer runs their own commerce agent with their own providers and credentials. Sessions are scoped per tenant — isolated by design, metered per tenant for billing.

<TenantArch
  tenants={[
    {
      id: "tenant_acme",
      label: "CARDS + SHIPPING",
      tone: "brand",
      servers: ["stripe", "correios", "nuvem-fiscal"],
      credentials: [
        { key: "stripe_key", value: "sk_live_aC******" },
        { key: "correios_token", value: "crs_7h******" },
      ],
    },
    {
      id: "tenant_loja",
      label: "PIX + MELHOR ENVIO",
      tone: "violet",
      servers: ["asaas", "melhor-envio", "z-api"],
      credentials: [
        { key: "asaas_key", value: "as_live_dX******" },
        { key: "me_token", value: "me_9k******" },
      ],
    },
  ]}
  apiSub="every tool call tagged with metadata.tenant_id for per-tenant billing"
  note="one CODESPAR_API_KEY · one vault · N tenants — tenant A cannot see tenant B's providers or credentials"
/>

## Prerequisites

```bash
npm install @codespar/sdk @codespar/openai openai next
```

<Callout>
You need **one** CodeSpar API key for your SaaS — not one per tenant. Tenant isolation happens inside CodeSpar via session metadata.
</Callout>

## Session factory

Wrap `codespar.create()` in a helper that takes a tenant config and injects `metadata.tenant_id`:

```typescript title="lib/commerce.ts"
import { CodeSpar } from "@codespar/sdk";

const codespar = new CodeSpar({ apiKey: process.env.CODESPAR_API_KEY! });

interface TenantConfig {
  id: string;
  servers: string[];
  metadata: Record<string, string>;
}

export async function createTenantSession(tenant: TenantConfig) {
  return codespar.create(tenant.id, {
    servers: tenant.servers,
    metadata: {
      tenant_id: tenant.id,
      ...tenant.metadata,
    },
  });
}
```

## API route per tenant

Use a dynamic route segment `[tenantId]` to scope every request to a tenant. The tenant config (which servers, which metadata) comes from your database:

```typescript title="app/api/commerce/[tenantId]/route.ts"
import { createTenantSession } from "@/lib/commerce";
import { getTools, handleToolCall } from "@codespar/openai";
import OpenAI from "openai";
import { NextResponse } from "next/server";

const openai = new OpenAI();

// In production, fetch from DB
const TENANTS: Record<string, { servers: string[] }> = {
  tenant_acme: { servers: ["stripe", "correios", "nuvem-fiscal"] },
  tenant_loja: { servers: ["asaas", "melhor-envio", "z-api"] },
};

export async function POST(req: Request, { params }: { params: { tenantId: string } }) {
  const tenant = TENANTS[params.tenantId];
  if (!tenant) return NextResponse.json({ error: "Tenant not found" }, { status: 404 });

  const { message } = await req.json();

  const session = await createTenantSession({
    id: params.tenantId,
    servers: tenant.servers,
    metadata: { source: "api" },
  });

  try {
    const tools = await getTools(session);
    // ... run agent loop with tools, return response ...
  } finally {
    await session.close();
  }
}
```

## Billing per tenant

Every tool call includes the `metadata.tenant_id` you set on session creation. Query CodeSpar's usage API and aggregate by tenant:

```typescript title="jobs/billing.ts"
// In your billing cron job
const response = await fetch("https://api.codespar.dev/v1/usage", {
  headers: { Authorization: `Bearer ${process.env.CODESPAR_API_KEY}` },
});

const data = await response.json();

// Group by tenant_id metadata
const perTenant = data.tool_calls.reduce((acc, call) => {
  const tenantId = call.metadata.tenant_id;
  acc[tenantId] = (acc[tenantId] || 0) + 1;
  return acc;
}, {});

// Bill each tenant based on usage
for (const [tenantId, callCount] of Object.entries(perTenant)) {
  await billTenant(tenantId, callCount);
}
```

<Callout>
The `metadata` field you set on a session is forwarded to every tool call log, making tenant-level billing and audit trails straightforward.
</Callout>

## Security considerations

- **Session isolation.** Each session has access only to the servers listed at creation time. Tenant A cannot reach Tenant B's providers — the runtime blocks it.
- **API key scoping.** Use separate CodeSpar keys per environment (test/prod), not per tenant. Tenant isolation is enforced by session scoping, not by key fragmentation.
- **Credential vault.** Each tenant's provider credentials are stored in CodeSpar's vault, encrypted per-organization. Tenants never see each other's secrets.
- **Rate limiting.** Apply per-tenant rate limits in your API route — CodeSpar won't stop tenant A from exhausting tenant B's budget unless you cap it upstream.

## Next steps

<NextStepsGrid items={[
  { label: "COOKBOOK", title: "Webhook Listener", description: "Combine with multi-tenant — scope webhook handlers per tenant.", href: "/docs/cookbooks/webhook-listener" },
  { label: "COOKBOOK", title: "Multi-Provider Agent", description: "Let each tenant pick their own provider mix via config.", href: "/docs/cookbooks/multi-provider" },
  { label: "CONCEPT", title: "Sessions", description: "Session metadata, lifecycle, and per-user isolation details.", href: "/docs/concepts/sessions" },
  { label: "CONCEPT", title: "Billing & Quotas", description: "Usage API, cost monitoring, tenant-level budgets.", href: "/docs/concepts/billing" },
]} />
