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_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 · BYOKws_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 + auditEvery 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:
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 });{
"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:
{ "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.