Seat-Based SaaS
Traditional seat-based pricing.
Org-scoped seat pools with user references, increment/decrement for countable resources, per-customer seat limit overrides for enterprise deals, and usage meters for features scoped to the workspace rather than the individual.
Limitr Cloud offers comprehensive pricing rules that can dynamically match segments of customers for discounting, markups, complete price overrides, promotions, and fully customizable enterprise pricing configurations — without policy document changes.
Policy
policy:
credits:
seat:
description: Workspace member seat
price: { amount: 15.00 } # $15/seat/month overage price
stof_units: int
# Flat monthly subscription credits — quantity-based, one increment per billing period
starter_subscription:
description: Monthly starter plan subscription fee
price: { amount: 49 }
stof_units: int
growth_subscription:
description: Monthly growth plan subscription fee
price: { amount: 149 }
stof_units: int
enterprise_annual:
description: Annual enterprise plan platform fee
price: { amount: 500 }
stof_units: int
plans:
starter:
label: Starter
period: monthly
default: true
entitlements:
subscription:
description: Monthly subscription charge ($49/mo)
limit:
credit: starter_subscription
mode: soft # first increment fires meter-overage → $49 charge
value: 0
seats:
description: Member seats — hard limit, 5 included
scope: org # enforced against the org customer, not the user
limit:
credit: seat
mode: hard
value: 5
increment: 1
minimum: 0
integrations:
description: Third-party integrations # boolean flag — no limit
growth:
label: Growth
period: monthly
entitlements:
subscription:
description: Monthly subscription charge ($149/mo)
limit:
credit: growth_subscription
mode: soft
value: 0
seats:
description: Member seats — soft limit, 10 included, overage billed per seat
scope: org
limit:
credit: seat
mode: soft # allows adding seats past limit; fires meter-overage per seat
value: 10
increment: 1
minimum: 0
integrations:
description: Third-party integrations
advanced_analytics:
description: Advanced analytics and exports
enterprise:
label: Enterprise
period: yearly
entitlements:
subscription:
description: Annual subscription charge
limit:
credit: enterprise_annual
mode: soft
value: 0
seats:
description: Member seats — negotiated limit, observe mode logs all additions
scope: org
limit:
credit: seat
mode: observe # no enforcement; all seat activity is metered for reporting
increment: 1
minimum: 0
integrations:
description: Third-party integrations
advanced_analytics:
description: Advanced analytics and exports
sso:
description: SAML/SSO access
audit_log:
description: Audit log access
Integration
import { Limitr } from '@formata/limitr';
import { readFileSync } from 'fs';
const policy = await Limitr.new(readFileSync('./policy.yaml', 'utf-8'), 'yaml');
// Subscription and seat overage billing
policy.addHandler('billing', (key: string, value: unknown) => {
if (key === 'meter-overage') {
const event = JSON.parse(value as string);
if (event.entitlement === 'subscription') {
billing.chargeSubscription(event.customer.id, event.customer.plan);
} else if (event.entitlement === 'seats') {
// event.overage is the number of seats over the soft limit
billing.chargeSeatOverage(event.customer.id, event.overage);
}
}
});
// ── Customer setup ─────────────────────────────────────────────────────────────
async function provisionOrg(orgId: string, plan: string, label: string) {
// Org is the billing and enforcement entity for seats
await policy.createCustomer(orgId, plan, 'org', label);
// Trigger the subscription charge for this billing period
await policy.ensureCustomerPlanQuantity(orgId);
}
async function provisionUser(userId: string, orgId: string, label: string) {
// User refs the org — scope:org entitlements resolve against the org's meters.
// User does not need a plan; it will be looked up from the org.
await policy.createCustomer(userId, undefined, 'user', label, [orgId]);
}
// ── Member management ──────────────────────────────────────────────────────────
async function addMember(userId: string, orgId: string) {
// increment() consumes one seat from the org's pool.
// scope:org means Limitr follows userId's refs to find the org customer.
const allowed = await policy.increment(userId, 'seats');
if (!allowed) {
const remaining = await policy.remaining(userId, 'seats');
return { error: 'Seat limit reached', code: 'SEAT_LIMIT', remaining: remaining ?? 0 };
}
await db.addOrgMember(userId, orgId);
return { success: true };
}
async function removeMember(userId: string, orgId: string) {
// decrement() releases one seat back to the org's pool
await policy.decrement(userId, 'seats');
await db.removeOrgMember(userId, orgId);
}
// ── Feature gating ─────────────────────────────────────────────────────────────
async function handleAnalyticsExport(userId: string) {
if (!await policy.check(userId, 'advanced_analytics')) {
return { error: 'Upgrade to Growth or Enterprise for analytics exports' };
}
return exportAnalytics(userId);
}
// ── Seat usage display ─────────────────────────────────────────────────────────
async function getSeatsDisplay(orgId: string) {
const used = await policy.value(orgId, 'seats');
const limit = await policy.limit(orgId, 'seats');
const remaining = await policy.remaining(orgId, 'seats');
const plan = await policy.plan(orgId);
return {
used,
limit,
remaining,
period: plan?.period,
canAddMore: remaining !== null && remaining > 0,
};
}
// ── Enterprise: per-org seat limit override ────────────────────────────────────
async function setEnterpriseSeats(orgId: string, contractedSeats: number) {
// Override the plan's seat limit for this specific org without changing the plan
await policy.createCustomerOverride(orgId, 'seats', contractedSeats);
}
Notes
Why scope: org instead of calling enforcement directly on the org customer? scope lets your application code stay uniform. Every call goes through the user's ID — policy.increment(userId, 'seats'). Limitr follows the user's refs list, finds the org customer, and operates against the org's meter. You never have to look up the org ID in your request handler. If a user isn't part of an org, the entitlement falls back to the user's own meter.
Why mode: observe on Enterprise seats instead of a high hard limit? Enterprise customers have negotiated seat counts set via override, not plan defaults. observe mode means the seat meter is always accurate for reporting and billing — you can invoice based on actual peak seat usage — without a policy failure if the contracted count wasn't set yet. Combine with createCustomerOverride() to enforce the contracted ceiling for any org that needs it.
Starter seats as hard, Growth as soft — Starter customers hit a wall at 5 seats with no overage path. Growth customers can add more and pay per seat. The distinction is intentional: Starter's limit is a plan boundary, not a billing trigger.
minimum: 0 on seat limits — without minimum, decrement() could push the meter negative if called incorrectly. minimum: 0 ensures the meter floors at zero regardless of how many decrements are called.
Subscription billing pattern — the subscription entitlement is a soft limit at value: 0. The first increment via ensureCustomerPlanQuantity() immediately fires meter-overage with event.entitlement === 'subscription'. Your billing handler maps this to the plan's subscription charge. Because the entitlement doesn't reset, this fires exactly once per customer per policy load — call ensureCustomerPlanQuantity() once per billing period renewal, not on every request.
Plan upgrades — when a customer upgrades from Starter to Growth, call setCustomerPlan(orgId, 'growth'). Meters reset by default. To preserve the current seat count through the upgrade, pass false as the third argument: setCustomerPlan(orgId, 'growth', false).