Docs

Billing

Billing has two independent axes: your account (AI budget + personal zone capacity) and each shared workspace (capacity only — nodes + meeting hours). Limits are enforced server-side at write time; over-limit returns 402 Payment Required with the current usage and an upgrade URL.

Two axes

Plans live in src/lib/billing/plans.ts. The account plan is stored in user_account_plans; the workspace plan in workspaces.plan.

Account plans (per user — AI is spent platform-wide)
account_free  — 100 ai_queries/mo · 500 personal-zone nodes · 1 mcp_agent
account_plus  $12/mo — 800 ai_queries/mo · 5,000 nodes · 5 mcp_agents
account_pro   $30/mo — 2,500 ai_queries/mo · ∞ nodes · ∞ mcp_agents · BYOK
Workspace plans (per shared project — capacity only, never per seat)
ws_free   — 500 nodes · 0 meeting hours/mo · 3 mcp_agents · ∞ members
ws_pro    $10/mo — 5,000 nodes · 8 meeting hours/mo (+$2.50/extra h) · ∞ agents
ws_scale  $50/mo — 50,000 nodes · 25 meeting hours/mo (+$2/extra h) · ∞ agents
ws_custom — contact sales · capacity to spec · SSO + audit

Every account gets a private personal zone (a workspace flagged is_personal) auto-provisioned at signup. Its node/agent caps come from the account plan, and it can never be shared.

AI is account-scoped

AI queries draw from the acting user'saccount budget, spent across the whole platform — your personal zone and every workspace you join. Workspaces never carry an AI budget. Agent/MCP queries are billed to the token creator's account.

Usage is metered per (subject_user_id, scope, period) in account_usage_counters, where scope is 'self' or a sponsoring company id (see Enterprise). Meeting minutes are metered per workspace in meeting_usage_counters.

Enforcement

Mutating routes guard their create path with helpers from src/lib/billing/enforce.ts. Node create branches on personal vs shared:

POST /api/nodes — enforcement excerpt
const wsEnt = await getWorkspaceEntitlements(storage.workspace.id);
const accountCtx = wsEnt.isPersonal
  ? await resolveAccountContext(actingUserId(gated), { billingAccountId: wsEnt.billingAccountId })
  : null;
const hit = await checkNodeCreate(wsEnt, accountCtx);
if (hit) return NextResponse.json(limitHitBody(hit), { status: 402 });
Folders never count. Demo workspaces skip the gate entirely.
402 Payment Required
{
  "error": "limit_reached",
  "message": "You've used 100/100 AI queries this month on the account_free account plan.",
  "limit": 100,
  "current": 100,
  "plan": "account_free",
  "kind": "ai_queries",
  "upgrade_url": "/pricing"
}

Enterprise — centralized company billing

Enterprise is a billing mode, not a tier. A billing_accounts row (a company) groups several workspaces (organizations.billing_account_id) and provisions per-member account-plan overrides in company_member_entitlements. A sponsored member's plan applies only inside that company's workspaces, and their AI usage is metered against the company scope. Manage it at /enterprise.

Stripe Checkout

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

Body
{ "kind": "account",   "plan": "account_plus", "cadence": "monthly" }
{ "kind": "workspace", "workspace_id": "<uuid>", "plan": "ws_pro", "cadence": "monthly" }
{ "kind": "enterprise", "billing_account_id": "<uuid>",
  "workspace_lines": [{ "plan": "ws_pro", "cadence": "monthly", "quantity": 3 }],
  "account_lines":   [{ "plan": "account_pro", "cadence": "monthly", "quantity": 5 }] }

Customer Portal at POST /api/billing/portal (same kind discriminator). Webhook at POST /api/webhooks/stripe branches on metadata.kind with full signature verification.

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. Limits still enforce — billing-off just means there's no upgrade path.