Skip to main content
Cookbooks

Multi-Tenant Agent

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.

1 min read
View MarkdownEdit on GitHub
TIME
~15 min
USE CASE
SaaSMarketplacesAgency
STACK
Next.jsOpenAI

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.

TENANT ISOLATION
Each tenant gets their own session, credentials, and billing
tenant_acme
CARDS + SHIPPING
SERVERS
stripecorreiosnuvem-fiscal
CREDENTIALS 🔒 vaulted
stripe_key: sk_live_aC******
correios_token: crs_7h******
tenant_loja
PIX + MELHOR ENVIO
SERVERS
asaasmelhor-envioz-api
CREDENTIALS 🔒 vaulted
asaas_key: as_live_dX******
me_token: me_9k******
CODESPAR API
Single account, isolated sessions
every tool call tagged with metadata.tenant_id for per-tenant billing
one CODESPAR_API_KEY · one vault · N tenants — tenant A cannot see tenant B's providers or credentials

Prerequisites

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

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

Session factory

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

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:

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.

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

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:

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)

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

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

Multi-Tenant Agent | CodeSpar