Skip to content

@venturekit-pro/billing

Terminal window
npm install @venturekit-pro/billing
import { definePlans } from '@venturekit-pro/billing';
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: 'support', enabled: false, description: 'Support' },
],
},
{
id: 'pro', name: 'Pro',
price: 2900, currency: 'USD', interval: 'month', active: true,
features: [
{ featureKey: 'projects', enabled: true, limit: 50, description: 'Projects' },
{ featureKey: 'support', enabled: true, description: 'Support' },
],
},
]);
import { getFeatureLimit, hasFeature } from '@venturekit-pro/billing';
const freePlan = plans.find(p => p.id === 'free')!;
getFeatureLimit(freePlan, 'projects'); // → 3
hasFeature(freePlan, 'support'); // → false
import { mapUsageToLineItems } from '@venturekit-pro/billing';
// (plan, usage) — usage is a Record<featureKey, quantity>
const lineItems = mapUsageToLineItems(plan, { projects: 12, apiRequests: 8500 });
import {
createSubscriptionManager, createUsageTracker, createFeatureGate, createInvoiceGenerator,
createMemorySubscriptionStore, createMemoryUsageStore, createMemoryInvoiceStore,
} from '@venturekit-pro/billing';
// Swap the memory stores for your own SubscriptionStore / UsageStore / InvoiceStore implementations.
const subscriptions = createSubscriptionManager({ store: createMemorySubscriptionStore(), plans, defaultTrialDays: 14 });
const usage = createUsageTracker(createMemoryUsageStore());
const gate = createFeatureGate({ plans, getUsage: (customerId, key) => usage.getUsage(customerId, key, periodStart, Date.now()) });
const invoices = createInvoiceGenerator({ store: createMemoryInvoiceStore(), prefix: 'INV', taxRate: 0.2, defaultCurrency: 'EUR' });
const sub = await subscriptions.create({ customerId, planId: 'pro' });
await usage.increment(customerId, 'apiRequests');
await gate.requireUsage('pro', 'projects', customerId); // throws when the plan limit is reached
await subscriptions.previewProration(sub.id, 'enterprise'); // what an upgrade would cost today
const invoice = await invoices.generateFromUsage(customerId, plan, await usage.getSummary(customerId, from, to));

Plan → what a tenant may do right now, with per-tenant overrides (a negotiated seat count, a temporary grant) layered on top, cached per subject, and refused from a route with the right HTTP status — 402 PLAN_UPGRADE_REQUIRED when the feature is not in the plan, 403 PLAN_LIMIT_REACHED (with details.limit / details.usage for the upgrade prompt) at the cap:

import { createEntitlementsResolver } from '@venturekit-pro/billing';
const entitlements = createEntitlementsResolver({
plans,
getPlanId: (tenantId) => subscriptions.currentPlanId(tenantId), // your lookup; null = no plan
getOverrides: (tenantId) => loadPlanOverrides(tenantId), // { seats: { limit: 500 } }
});
const ent = await entitlements.forSubject(ctx.tenant!.id);
ent.require('api_access');
await ent.requireWithinLimit('seats', () => members.count(ctx.tenant!.id));
const quotas = ent.toQuotas({ seats: 'maxUsers' }); // feeds tenancy's createQuotaMiddleware

Call entitlements.invalidate(tenantId) after a plan change. The errors carry the runtime’s structural marker, so the handler boundary renders them with their status even though this package does not depend on @venturekit/runtime.

The package declares its SQL migrations via "vk": { "migrations": "src/migrations" }, so vk migrate applies them automatically; getBillingMigrationsDir() returns the directory for a custom runner.

  • PlanDefinition, PlanFeature — plan configuration types
  • Subscription, SubscriptionStore, ProrationResult, UsageRecord, UsageStore, Invoice, InvoiceStore, FeatureCheckResult — lifecycle, metering, invoicing and gating types
  • Entitlements, EntitlementsResolver, EntitlementOverrides, LimitCheck, EntitlementError — entitlement types
  • @venturekit/core — required