Example: SaaS seats (SQLite)
examples/saas-seats: seats and storage, billed per billing cycle anchored on the subscription day, on SQLite.
bunx vitest run examples/saas-seats/** * 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. */Price table
Section titled “Price table”/** 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.
Catalog and getRate
Section titled “Catalog and getRate”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.
Closing a cycle
Section titled “Closing a cycle”/** * 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 };},integratereduces level samples to two quantities: seat-seconds (time_weighted) and peak GB (max, B10).- One multi-line
observewith a period ref ({account}:{cycle key}) andat: cycle.to − 1(A98). minimumCommittops the enterprise plan up to its monthly minimum (B12), written withmetering.commit(plan).
Both steps are idempotent: a cron that closes the same cycle twice writes nothing twice.