Notifications
A notification is a rule in the policy that watches events: "tell me when a customer has less than 20% left", "page someone when an enterprise customer hits a hard limit". Because it's part of the policy, it's versioned and shipped with it, and it runs the same on every server.
import { Limitr } from '@formata/limitr';
const limitr = await Limitr.new(`
policy: {
credits: { request: { label: 'Request' } }
plans: {
free: {
default: true
entitlements: {
api: {
limit: {
credit: 'request', value: 100
resets: true, reset_sch: 'monthly:1'
}
}
}
}
}
notifications: {
// Matches when a call leaves less than 20% of the limit. No fire function, so the event is
// sent again under the notification's name.
api_running_low: {
fn matches(type: str, event: obj) -> bool {
type == 'meter-changed' && event.entitlement == 'api' &&
(event.remaining ?? 0) < (event.meter.limit ?? 0) * 0.2
}
}
}
}`);
// A notification added at runtime, with a fire function that runs in the policy.
await limitr.setNotifications(`
api_blocked: {
fn matches(type: str, event: obj) -> bool { type == 'meter-limit' }
fn fire(event: obj) { pln('blocked at ' + event.meter.value + ' of ' + event.meter.limit); }
}`);
limitr.addHandler('alerts', (name, value) => {
if (name === 'api_running_low') {
const event = JSON.parse(value as string);
console.log(`${event.customer.id} has ${event.remaining} requests left`);
}
});
await limitr.ensureCustomer('ada');
for (const n of [70, 15, 10, 10]) await limitr.allow('ada', 'api', n);
It prints:
ada has 15 requests left
ada has 5 requests left
blocked at 95 of 100
Writing a notification
A notification is an object under policy.notifications with:
fn matches(type: str, event: obj) -> bool(required): whether this event is one it cares about.typeis the event name, andeventis the payload (see Events).fn fire(...)(optional): what to do when it matches. It can take(name: str, event: obj),(event: obj), or nothing, and it can beasync.
With no fire, a match sends the event again under the notification's name, so your app's handlers receive it
as api_running_low instead of meter-changed. That's the most common setup: the condition lives in the policy, and
the action lives in your app.
With a fire, the action runs in the policy. It can call functions your app registers (Ex.
App?::page_oncall(event.customer.id), see Call your app from a policy).
Notifications receive meter events (meter-changed, meter-overage, meter-limit, meter-governed,
meter-reset), cap events (cap-limit, cap-threshold-crossed), and feature-checked.
Adding notifications at runtime
setNotifications(contents, format = 'stof') (set_notifications in Stof) adds every object in contents as a
notification. A name that already exists is replaced. They take effect on the next call:
import { readFileSync } from 'node:fs';
await limitr.setNotifications(readFileSync('./notifications.stof', 'utf-8'));
Event handler functions
Any Stof function in the document with an event's name as an attribute runs for that event, without a matches:
#[meter-overage]
fn on_overage(event: obj) {
App?::bill(event.customer.id, event.entitlement, event.overage_price);
}
It takes the event, or nothing. Use notifications for conditions, and attribute functions for "every event of this kind".
Writing conditions
- Default missing fields. Comparing with null is false, but arithmetic with null is an error in Stof, so write
(event.meter.limit ?? 0) * 0.8, notevent.meter.limit * 0.8. A flag-only entitlement has no limit. - Keep them cheap.
matchesruns for every event. Checktypefirst. - Don't keep the event. Event objects are dropped after delivery; copy what you need.
| Condition | matches |
|---|---|
| 80% of an allowance used | type == 'meter-changed' && (event.meter.value ?? 0) >= (event.meter.limit ?? 0) * 0.8 |
| A Growth customer hit a hard limit | type == 'meter-limit' && event.customer.plan == 'growth' |
| Any limit on seats | event.entitlement == 'seats' && (type == 'meter-limit' || type == 'meter-overage') |
| Fewer than 1,000 tokens left | type == 'meter-changed' && event.entitlement == 'chat' && (event.remaining ?? 0) < 1000 |
| A spend cap crossed | type == 'cap-threshold-crossed' |
| Overage on one plan | type == 'meter-overage' && event.plan == 'enterprise' |
Once, not every call. A condition like "80% used" matches on every call past 80%. To act once per period, use an
observe_only spend cap with its value at the threshold: cap-threshold-crossed fires once.