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.
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:
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 });
}
}The 402 body shape:
{
"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:
{
"workspace_id": "<uuid>",
"plan": "pro" | "team",
"cadence": "monthly" | "yearly",
"seats": 5 // required for team
}{ "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.
GET /api/founder-ltd/status
{ "total": 200, "sold": 14, "remaining": 186, "sold_out": false }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.