---
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.
---

<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 carries the `metadata.tenant_id` you set on session creation, which is what makes a per-tenant view possible at all.

<Callout type="warn">
`/v1/usage` is **not** an endpoint your billing job can call. It sits in the
service-auth subtree, so a `csk_` key answers `401`, and the credential that
does open it is CodeSpar's platform secret — it never leaves our
infrastructure. Read the aggregate in the dashboard at
[/dashboard/billing](https://codespar.dev/dashboard/billing).
</Callout>

For your own per-tenant counters, count on the side you control. You already
know the tenant at session creation, so record the attribution there rather
than trying to reconstruct it from an endpoint you cannot reach:

```typescript title="jobs/billing.ts"
// At session creation you already hold the tenant id — stamp it on CodeSpar
// for audit AND on your own meter in the same step.
const session = await cs.create({
  servers: ["asaas"],
  metadata: { tenant_id: tenantId },
});
await meter.increment(tenantId, { sessionId: session.id });

// Tool-call detail for one session stays on the Bearer surface, so a
// reconciliation job can walk what a tenant's session actually did:
//   GET /v1/sessions/:id/tool-calls
//   GET /v1/tool-calls?since=...   (project-scoped, newest first)
```

<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-account. 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: "All cookbooks", description: "Browse the full set of runnable recipes.", href: "/docs/cookbooks" },
  { 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" },
]} />
