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.

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 sessions.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.sessions.create({
    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 includes the metadata.tenant_id you set on session creation. Query CodeSpar's usage API and aggregate by tenant:

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);
}

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