Context
The plan matrix (#17) enforced: one limits module that every gate consults, plus usage metering users can see. Limits fail soft and sell the upgrade; they never 500 and never silently drop data without accounting for it.
Scope
app/Billing/limits.ts: the tier matrix as typed config (single source of truth, imported by #17's pricing page so marketing numbers can never drift from enforcement);can(user|project, capability)+remaining(project, meter)helpers.- Metering: monthly event counts per project (increment on ingest #6, stored via a model, month-keyed); report/project/member counts derived live; meter reads cached.
- Enforcement points:
- Ingest (#6): over-quota projects get
429with a machine-readable reason + grace behavior (accept N% overage once per month, then reject) - document the exact policy indocs/limits.md. - Report/project/member creation (#4, #9): friendly in-UI blocks with upgrade prompts.
- Schedules + XLSX (#15), unbranded shares + embeds (#16): tier checks via
can().
- Ingest (#6): over-quota projects get
- Retention: a scheduled pruning job deleting events past the tier's retention window (and rollups per #10's rules), batched, with a dry-run mode and logged counts; downgrades prune on next run - warned in the UI at downgrade time.
- Usage UI: a usage panel on the billing page (#17) and a compact meter in the project view - events this month vs quota, retention window, with honest near-limit states (80%, 100%).
- Emails: near-quota (80%) and over-quota notifications via
app/Mail/- once per threshold per month, not a nag storm. - Feature tests: every enforcement point at limit-1 / limit / limit+1, month rollover, downgrade retention prune, grace policy.
Acceptance
- A Free project at its event quota gets documented
429s, sees an accurate meter, receives one warning email, and upgrading to Hobby in Stripe test mode immediately unblocks ingest.
Depends on #6, #17. Gates #15, #16. Part of #1 (M6 Monetization). Ground rules: see #1.