Skip to content

Example: SaaS seats (SQLite)

examples/saas-seats: seats and storage, billed per billing cycle anchored on the subscription day, on SQLite.

Terminal window
bunx vitest run examples/saas-seats
examples/saas-seats/src/billing.ts
/**
* SaaS seats and storage, billed per billing cycle on SQLite.
*
* - Seats and storage are levels, not events: the app keeps samples and, at cycle close, turns them into
* one quantity each with `integrate()` (seat-seconds; max GB).
* - Proration is `per`: a seat's monthly price is spread over the cycle's seconds, so a seat added
* halfway costs half.
* - Prices come from a layered table (`layeredRates`): account > plan > list, per currency (EUR accounts
* read EUR rows; the `Rate` itself has no currency).
* - Enterprise plans have a monthly minimum commitment, settled with `minimumCommit` after the usage.
* - The cycle is anchored on the subscription day (`cycleWindow`/`cycleKey`), not the calendar month.
*/
examples/saas-seats/src/billing.ts
/** The price table. In production these are rows in your database, edited by an admin UI. */
export const PRICE_ROWS: PriceVersion[] = [
{ id: "seat-list-usd", meter: "seats", scope: "list:USD", effectiveFrom: 0, rate: seat(10_000_000) },
{ id: "seat-list-eur", meter: "seats", scope: "list:EUR", effectiveFrom: 0, rate: seat(9_000_000) },
{ id: "seat-ent-usd", meter: "seats", scope: "plan:enterprise:USD", effectiveFrom: 0, rate: seat(8_000_000) },
{ id: "gb-list-usd", meter: "storage/gb", scope: "list:USD", effectiveFrom: 0, rate: storage(250_000) },
{ id: "gb-list-eur", meter: "storage/gb", scope: "list:EUR", effectiveFrom: 0, rate: storage(230_000) },
];

Scopes are account:{id} > plan:{plan}:{currency} > list:{currency}. EUR accounts read EUR rows; the Rate itself has no currency (B17). The first 10 GB are included.

examples/saas-seats/src/billing.ts
const table = layeredRates<Context>({
loadRows: (meter) => rows.filter((r) => r.meter === meter),
scopes: (ctx) => [`account:${ctx.accountId}`, `plan:${ctx.plan}:${ctx.currency}`, `list:${ctx.currency}`],
cacheMs: 60_000,
});
const metering = buildMetering()
.context(Context)
.refs(["period", "adjustment"])
.meter("seats")
.meter("storage/gb")
.getRate(async (meter, dims, ctx, at) => {
const found = await table(meter, dims, ctx, at);
if (!found) return null;
const cycle = cycleWindow({ at, anchor: ctx.cycleAnchor });
const usedSoFar = store.usedSoFar({ account: ctx.accountId, meter, from: cycle.from, to: cycle.to });
if (meter === "seats") {
// prorate: the monthly price is per seat per cycle, quantity is seat-seconds
const seconds = (cycle.to - cycle.from) / 1000;
return { rate: { ...found.rate, tiers: found.rate.tiers.map((t) => ({ ...t, per: seconds })) }, usedSoFar };
}
return { rate: found.rate, usedSoFar };
})
.commit(store.commit);

A seat’s price is its monthly price per cycle; because the quantity is seat-seconds, getRate sets every tier’s per to the cycle’s seconds. A seat added halfway costs half (B9). usedSoFar is read over the cycle window.

examples/saas-seats/src/billing.ts
/**
* Closes the cycle that contains `at` (normally called by a cron right after the cycle ends).
* Idempotent: the refs are keyed by account and cycle.
*/
async closeCycle(i: { ctx: Context; at: number; seats: readonly Sample[]; storageGb: readonly Sample[] }) {
const cycle = cycleWindow({ at: i.at, anchor: i.ctx.cycleAnchor });
const key = cycleKey({ at: i.at, anchor: i.ctx.cycleAnchor });
const seatSeconds = integrate({ samples: i.seats, window: cycle, mode: "time_weighted", unit: "s" });
const maxGb = integrate({ samples: i.storageGb, window: cycle, mode: "max" });
const at = cycle.to - 1;
const usage = await metering.observe(
[
{ meter: "seats", quantity: seatSeconds },
{ meter: "storage/gb", quantity: maxGb },
],
{ type: "period", id: `${i.ctx.accountId}:${key}` },
i.ctx,
{ at },
);
if (!usage.ok) return { cycle: key, usage, minimum: null };
const spent = store.spent({ account: i.ctx.accountId, from: cycle.from, to: cycle.to });
const plan = minimumCommit({
account: i.ctx.accountId,
minimumMicroUsd: MINIMUM[i.ctx.plan],
spentMicroUsd: spent,
period: key,
at,
refType: "adjustment",
});
const minimum = await metering.commit(plan);
return { cycle: key, usage, minimum, shortfall: plan.ledger[0]?.amount ?? 0 };
},
  1. integrate reduces level samples to two quantities: seat-seconds (time_weighted) and peak GB (max, B10).
  2. One multi-line observe with a period ref ({account}:{cycle key}) and at: cycle.to − 1 (A98).
  3. minimumCommit tops the enterprise plan up to its monthly minimum (B12), written with metering.commit(plan).

Both steps are idempotent: a cron that closes the same cycle twice writes nothing twice.