Synexa
How It Works

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: count and lastReset. Anonymous usage is tracked at anonymous_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.