Docs

Billing

Free-tier limits are enforced server-side at write time. When a limit is hit, the API returns 402 Payment Required with a JSON body that includes the current usage and the upgrade URL. There's no silent throttling and no surprise auto-charging.

Plans

Plans live in src/lib/billing/plans.ts. Each plan declares its limits in code — the database stores only the plan id and seats.

Plan limits
free       — 500 nodes · 1 workspace · 100 ai_queries/mo · 1 mcp_agent
pro $19/mo — unlimited nodes · 5 workspaces · 1,500 ai_queries/mo · 5 mcp_agents · BYOK
team $29   — unlimited · ∞ workspaces · 3,000 ai_queries/seat/mo · ∞ mcp_agents (3–15)
business   — unlimited · ∞ · fair use · ∞ · SSO + audit (min 5 seats)
lifetime_pro — Pro limits, paid once via founder LTD ($199, capped at 200 seats)

Enforcement

Every mutating route that consumes a quota wraps its create path with a guard from src/lib/billing/enforce.ts:

POST /api/nodes — enforcement excerpt
if (storage.backend === "supabase" && rest.kind !== "folder") {
  const ent = await getEntitlements(storage.workspace.id);
  const hit = await checkNodeCreate(ent);
  if (hit) {
    return NextResponse.json(limitHitBody(hit), { status: 402 });
  }
}
Folders never count against the cap. Demo workspaces skip the gate entirely.

The 402 body shape:

402 Payment Required
{
  "error": "limit_reached",
  "message": "Workspace is at the 500-node limit for the free plan.",
  "limit": 500,
  "current": 500,
  "plan": "free",
  "kind": "nodes",
  "upgrade_url": "/pricing"
}

Usage counters

Monthly buckets (UTC YYYY-MM) live in usage_counters(workspace_id, period, kind, count). The chat route increments kind='ai_query' on every successful call; the node-create route increments kind='node_create'. RLS scopes reads to workspace members; writes go through the service-role admin.

Stripe Checkout

When STRIPE_SECRET_KEY and the relevant STRIPE_PRICE_* env vars are set, POST /api/billing/checkout creates a Checkout session:

Body
{
  "workspace_id": "<uuid>",
  "plan": "pro" | "team",
  "cadence": "monthly" | "yearly",
  "seats": 5            // required for team
}
Response
{ "url": "https://checkout.stripe.com/c/pay/cs_test_..." }

Customer Portal at POST /api/billing/portal. Webhook at POST /api/webhooks/stripe handles checkout.session.completed, customer.subscription.updated, customer.subscription.deleted with full signature verification.

Founder LTD

A one-time $199 lifetime-Pro license, capped at 200 sales. The counter is the live row count in founder_ltd_sales(status='paid'); refunds free a seat.

Public status endpoint
GET /api/founder-ltd/status

{ "total": 200, "sold": 14, "remaining": 186, "sold_out": false }
CDN-cached 10s, stale-while-revalidate 30s.

The checkout endpoint (POST /api/founder-ltd/checkout) reads the counter before creating the Stripe session and returns 410 Gone when sold out. The Stripe webhook branches on metadata.product === 'founder_ltd' to record the sale and flip the workspace plan to lifetime_pro.

When billing is disabled

The Stripe SDK is a soft dependency. If stripe isn't installed or STRIPE_SECRET_KEY is missing, /api/billing/* returns 503 billing_disabled. Free-tier limits still enforce — billing-off mode just means there's no upgrade path.