AUTH4D-309 Monthly active user alerts
Project owners get an email when the project’s monthly active users (MAU) reach 80% of the plan’s
maxMonthlyActiveUsers, and one more when they reach 100%. Each threshold sends at most one email
per project per UTC calendar month. The console shows the same numbers in a banner. The limit
stays report-only: sign-in keeps working over the limit.
MAU is the AUTH4D-39 count: distinct end users of the project with a session created or refreshed in the current UTC calendar month.
When alerts are checked
Every usage report from createEntitlementService(...).getUsageReport (packages/tenancy) goes to
the onUsageReport hook, which the API wires to the usage alert service
(apps/api/src/modules/usage-alerts). Reports come from two places:
GET /management/tenants/{tenantId}/usage, which the console calls on the project overview and billing screens. The request waits for the alert check, because a Worker may stop work left running after its response. A failing check never fails the usage response.- An hourly sweep on the first lifecycle cron run of each hour (
*/10 * * * *, minute 0 to 9). It checks up to 500 active customer projects that have enough players this month to reach 80% of the smallest plan and have not had their 100% email this month, busiest first.
Rules
- The threshold is the highest one reached, in integer arithmetic: 80 when
count * 100 >= limit * 80, 100 whencount >= limit. - A threshold is sent once per
(project, month, threshold), the primary key ofusage_alerts(migration 0040). A higher or equal threshold already sent this month covers a lower one, so a project that jumps straight past 80% gets only the 100% email. - After a mid-month plan change the percentage is measured against the new plan, but a threshold already sent that month is not sent again.
- The email goes to the verified console email addresses of the project’s owners (at most 5), not to admins, viewers or the billing contact. It is queued through the encrypted email outbox under the control-plane console client, like the billing emails.
- The stored row, not the report, decides the email’s numbers. If the queue cannot take a message,
the row stays
pendingand the next report republishes exactly the same message. A pending row older than 3 days is started again with fresh numbers. - With
BILLING_ENABLEDother than"true"(or billing misconfigured) the email and the banner show usage only, with no upgrade link. - A project with no owner address still records the send, with
recipient_count0, so it is not retried every hour.
Usage response
GET /management/tenants/{tenantId}/usage (scope tenant:read, any project role) gains an
additive usageAlert object next to usage:
{
"usage": {
"tenantId": "ten_...",
"planId": "free",
"source": "free",
"graceEndsAt": null,
"period": { "start": "2026-10-01T00:00:00.000Z", "end": "2026-11-01T00:00:00.000Z" },
"monthlyActiveUsers": { "count": 812, "limit": 1000, "overLimit": false },
"applications": { "count": 1, "limit": 2, "overLimit": false }
},
"usageAlert": { "threshold": 80, "upgradeAvailable": true }
}
usageAlert.threshold:80,100, ornullbelow 80%.usageAlert.upgradeAvailable:trueonly while billing is enabled; the console then links to the billing screen’s plans.
The response is Cache-Control: no-store, as before. Callers outside the project get 403, and
no alert is checked.
Template billing-usage-alert in packages/email, sent from the auth4.dev sender:
- Subject at 80%:
{Project} has used 80% of its monthly active users. - Subject at 100%:
{Project} has reached its monthly active user limit. - Body: the MAU count and limit, the plan and month, “Sign-ins keep working when a project goes over its limit.” and the date the count starts again.
- With billing on: “To raise the limit, move the project to a larger plan.” and a Compare plans
link to
{CONSOLE_ORIGIN}/projects/{tenantId}/billing. With billing off there is no link.
Console
The project overview and the billing screen show a status banner at 80% and 100%: the heading, the
count and limit for this month, and that sign-ins keep working. While billing is on, the overview
links to the billing screen (Compare plans) and the billing screen’s button moves focus to its
Plans section. Below 80%, on errors, or with an API that sends no usageAlert, nothing is shown.
Audit
Each send writes billing.usage_alert.sent (outcome success, target type tenant) to the
project’s own audit log, in the same D1 batch that marks the row sent, so a send is audited
exactly once even when two reports race. Metadata holds no address:
{
"threshold": 80,
"period_month": "2026-10",
"plan_id": "free",
"monthly_active_users": 812,
"user_limit": 1000,
"recipient_count": 2,
"upgrade_link": true
}
The event name is added to BILLING_AUDIT_EVENTS in packages/contracts.
Tests
tests/integration/auth4d-309.usage-alerts.test.ts: thresholds, one email per owner at 80% and 100%, repeat reports sending nothing, billing off, project isolation, queue failure retry, and month rollover through the sweep, on a D1 built from every migration.tests/integration/auth4d-309.console-usage-banner.test.ts: the banner andusageAlertparsing.packages/email/src/usage-alert-email.test.ts: the email wording, the link rules and validation.
Source: docs/api-spec/auth4d-309.md