Skip to content

Billing

VentureKit provides billing support through @venturekit-pro/billing with plan definitions, feature limits, and usage-to-invoice mapping.

Terminal window
npm install @venturekit-pro/billing

@venturekit-pro/billing handles plan logic and invoicing, not payment processing. It provides:

  • Plan definitions with feature limits
  • Feature checking (hasFeature, getFeatureLimit)
  • Usage-to-invoice-item mapping
  • Auto-migration support for billing database tables

Integrate any payment provider separately when needed.

import { definePlans } from '@venturekit-pro/billing';
// Each plan requires id/name/price/currency/interval/active and a `features`
// array. Each feature is `{ featureKey, enabled, limit?, unitPrice?, description }`.
// Use `limit: -1` for unlimited numeric features. Boolean features set
// `enabled: true` (no `limit`).
const plans = definePlans([
{
id: 'free', name: 'Free',
price: 0, currency: 'USD', interval: 'month', active: true,
features: [
{ featureKey: 'projects', enabled: true, limit: 3, description: 'Projects' },
{ featureKey: 'storage', enabled: true, limit: 1_000_000_000, description: 'Storage (bytes)' },
{ featureKey: 'apiRequests', enabled: true, limit: 10_000, description: 'API requests / month' },
{ featureKey: 'customDomains', enabled: false, description: 'Custom domains' },
{ featureKey: 'support', enabled: false, description: 'Priority support' },
],
},
{
id: 'pro', name: 'Pro',
price: 2900, currency: 'USD', interval: 'month', active: true,
features: [
{ featureKey: 'projects', enabled: true, limit: 50 },
{ featureKey: 'storage', enabled: true, limit: 100_000_000_000 },
{ featureKey: 'apiRequests', enabled: true, limit: 1_000_000 },
{ featureKey: 'customDomains', enabled: true },
{ featureKey: 'support', enabled: true },
],
},
{
id: 'enterprise', name: 'Enterprise',
price: 0, currency: 'USD', interval: 'year', active: true, // "contact us" — 0 = priced manually
features: [
{ featureKey: 'projects', enabled: true, limit: -1 }, // -1 = unlimited
{ featureKey: 'storage', enabled: true, limit: -1 },
{ featureKey: 'apiRequests', enabled: true, limit: -1 },
{ featureKey: 'customDomains', enabled: true },
{ featureKey: 'support', enabled: true },
],
},
]);

Use in handlers to enforce plan limits:

import { handler, ForbiddenError } from '@venturekit/runtime';
import { getFeatureLimit, hasFeature } from '@venturekit-pro/billing';
export const main = handler(async (_body, ctx, logger) => {
const userPlanId = 'free'; // Resolve from tenant/user
const plan = plans.find(p => p.id === userPlanId);
if (!plan) throw new ForbiddenError('Unknown plan');
// Check boolean feature — (plan, featureKey)
if (!hasFeature(plan, 'customDomains')) {
throw new ForbiddenError('Custom domains require a Pro plan');
}
// Check numeric limit — (plan, featureKey)
const maxProjects = getFeatureLimit(plan, 'projects') ?? 0;
const currentCount = await getProjectCount(ctx.user!.id);
if (maxProjects !== -1 && currentCount >= maxProjects) {
throw new ForbiddenError(`Project limit reached (${maxProjects})`);
}
return { allowed: true };
}, { scopes: ['projects.write'] });

Generate invoice line items from usage data:

import { mapUsageToLineItems } from '@venturekit-pro/billing';
// (plan, usage) — usage is a Record<featureKey, quantity>
const lineItems = mapUsageToLineItems(plan, {
apiRequests: 8_500,
storage: 12_000_000_000,
});
// Returns structured line items for invoice generation

When @venturekit-pro/billing is installed, vk migrate discovers and applies billing-related database tables automatically:

import { getBillingMigrationsDir } from '@venturekit-pro/billing';
const migrationsDir = getBillingMigrationsDir();

createFeatureGate answers “is feature X in plan P”. A product usually needs a little more around it: who is on which plan, per-tenant overrides (a negotiated “500 seats on Pro”, a temporary grant), a cache, and refusals that come back as the right HTTP answer. That is the entitlements layer:

import { createEntitlementsResolver } from '@venturekit-pro/billing';
export const entitlements = createEntitlementsResolver({
plans,
getPlanId: (tenantId) => subscriptions.currentPlanId(tenantId), // subscription row, tenant column, …
getOverrides: (tenantId) => loadPlanOverrides(tenantId), // { seats: { limit: 500 }, sso: { enabled: true } }
ttlMs: 60_000,
});
export const main = handler(async (body, ctx) => {
const ent = await entitlements.forSubject(ctx.tenant!.id);
ent.require('api_access'); // 402 PLAN_UPGRADE_REQUIRED
const seats = await ent.requireWithinLimit('seats', () => members.count(ctx.tenant!.id)); // 403 PLAN_LIMIT_REACHED
await members.add(ctx.tenant!.id, body.email);
return { remainingSeats: seats.remaining };
}, { scopes: ['members.write'] });

EntitlementError carries the runtime’s structural error marker, so the handler boundary renders it as { error: { code, message, details } } with details.limit / details.usage for the upgrade prompt — the SPA can show “You’re at 10/10 seats — upgrade” from the response alone. Call entitlements.invalidate(tenantId) when a plan changes, and ent.toQuotas({ seats: 'maxUsers' }) to hand the finite limits to tenancy’s quota middleware below.

Billing works well with @venturekit-pro/tenancy for per-tenant plan enforcement:

import { createTenantMiddleware, createQuotaMiddleware } from '@venturekit-pro/tenancy';
import { getFeatureLimit } from '@venturekit-pro/billing';
export const main = handler(async (_body, ctx, logger) => {
const tenant = ctx.tenant!;
const planId = tenant.metadata.plan as string;
const plan = plans.find(p => p.id === planId)!;
const limit = getFeatureLimit(plan, 'apiRequests');
return { tenantId: tenant.id, plan: plan.id, apiRequestLimit: limit };
}, {
scopes: ['api.read'],
middleware: [
createTenantMiddleware({
// `strategy` = data isolation model · `resolution` = how the tenant id is extracted
config: { strategy: 'shared', resolution: 'subdomain' },
lookupTenant: async (id) => loadTenant(id),
}),
createQuotaMiddleware({
quotaKey: 'apiRequests',
checkUsage: async (tenantId) => getMonthlyUsage(tenantId),
}),
],
});