Skip to content

Plans and limits

When your product has billing, each plan says how many of something an organization may have (limits) and which capabilities it unlocks (features). Core already counts members and units. Use this page when your product adds something to count (projects, orders, todos) or a feature only some plans get (reports, exports).

Limits refuse additions only: an organization above its plan’s limit keeps everything and can still work; only new additions are refused with 403 PLAN_LIMIT_REACHED. The plan and usage numbers (details) go only to a caller who holds Billing.Read organization-wide (and a platform admin); anyone else gets the same status and code with a generic message and no details, and the client shows the generic limit text. Core never limits a platform admin on members, units and invitations; your own count skips them when you pass { caller } (below). Without billing in the product config, both calls below do nothing.

Name them in the plans

In product.config.ts, under billing.plans:

{ key: 'free', limits: { members: 3, units: 0, projects: 5 }, features: [], paid: false },
{ key: 'pro', limits: { members: 25, units: 20, projects: null }, features: ['reports'], paid: true },

A missing limit or null is unlimited. Plan and limit names in the UI come from billing.plans.<key> and billing.limits.<key> in your product’s translation files.

Count something

  1. Register a counter once, in your product’s API library, and pass it to core:

    // libs/<product>/api/...
    export const projectsPlanUsageCounter: PlanUsageCounter = {
    key: 'projects',
    count: (tx, organizationId) =>
    TenantContext.runAs(organizationId, () => (tx as ProductTx).project.count()),
    };
    // apps/<product>/api/src/app/app.module.ts
    CoreModule.forRoot({ ..., planUsageCounters: [projectsPlanUsageCounter] })
  2. Where a project is created, inside the write’s transaction, check the limit first:

    await this.prisma.$transaction(async (tx) => {
    await this.limits.assertWithin(tx, organizationId, 'projects');
    return tx.project.create({ data });
    });

    PlanLimits comes from @core/api-billing. Pass { caller: request.user } as a fourth argument to let a platform admin past the limit, as core does for members and units; without it (Keelmint’s todos) a platform admin’s create into an organization (X-Organization-Id) counts like anyone’s. It takes a per-organization lock for that limit, so two concurrent creates cannot both pass at the last free slot. Keelmint’s todos (libs/keelmint/api/todos) are the worked example.

Gate a feature

Put @RequireFeature('reports') (from @core/api-security) on a controller or a route, and list 'reports' in the features of the plans that include it. A caller on another plan gets 403 FEATURE_NOT_IN_PLAN (with the plan in details only for a Billing.Read holder organization-wide or a platform admin); the client shows a toast, with an Upgrade link for those billing readers only (anyone else could not upgrade, so they get the plain message). Core gates its organization audit log with the audit feature.