Usage Quotas
A metered usage system that limits actions per subscription tier, enforced atomically so concurrent requests can't bust the limit.
How it Works
- Per-tier limits and reset periods live in
config/quota.config.ts(e.g.free: { limit: 5, resetPeriod: "monthly" },pro: { limit: 50 },enterprise: { limit: -1 }for unlimited). - Counters are stored per user, per resource at
users/{userId}/usage/{resourceKey}with two fields:countandlastReset. Anonymous usage is tracked atanonymous_usage/{ipFingerprint}/{resourceKey}/counter. - The limit itself is not stored on the document — it's read from config at check time, so changing a tier's limit takes effect immediately.
Atomic Operations
The core logic is in lib/usage/quota.ts. It uses a Firestore transaction to read-and-increment in one step, so two simultaneous requests can't both pass when one slot remains.
checkAndReserveQuota(params)
Reserves one unit before you do the billable work:
import { checkAndReserveQuota } from "@/lib/usage/quota";
import { quotaConfig } from "@/config/quota.config";
const quota = await checkAndReserveQuota({
userId, // null for anonymous (tracked by ipFingerprint)
resourceKey: "ai_generation",
userTier, // e.g. "free" | "pro" — a key in quotaConfig.tiers
ipFingerprint, // required in practice when userId is null
config: quotaConfig,
});
if (!quota.allowed) {
// quota.reason is "quota_exceeded" or "invalid_tier"
return new Response("Quota exceeded. Please upgrade.", { status: 429 });
}
// ...proceed with the billable action; quota.remaining tells you what's left
refundReservedQuota(params)
If the action ultimately fails, give the slot back so a failure doesn't cost the user:
import { refundReservedQuota } from "@/lib/usage/quota";
await refundReservedQuota({
userId,
resourceKey: "ai_generation",
userTier,
config: quotaConfig,
startMs: quota.startMs, // returned from checkAndReserveQuota
});
app/api/ai/generate/route.ts is a complete reference: reserve → call the model → refund on error.
The AI example route is the only bundled product action currently wired to
quota enforcement. The quota library and profile usage widget are reusable
infrastructure; adding another billable action requires calling the reservation
API in that action and selecting its resource in
config/usage-resources.config.ts if it should appear in the profile widget.
For anonymous usage, derive a stable, privacy-conscious fingerprint from the
trusted client IP and pass ipFingerprint. Omitting it uses the fallback
"unknown", which merges all anonymous callers into one quota bucket and is not
appropriate for production enforcement.
For streaming work, refund only when the provider produced no usable output. The AI reference route follows that rule so a partially delivered response is not both consumed and refunded.
Automatic Resets
Resets are based on the UTC calendar period for the tier's resetPeriod:
monthly— resets at the start of each UTC month.daily— resets at the start of each UTC day.never— never resets (good for lifetime/unlimited tiers).
On each check, if the stored lastReset is older than the current period start, the count is treated as 0 — no scheduled job required.
Next: Read about Deployment.