AI
Implementing AI usage limits and credits in Next.js
Enforce a monthly credit cap in Next.js by summing a Prisma generation ledger and reading the user's plan.
A usage limit is a number you can recompute from rows you already store. The AI SaaS Starter does not keep a creditsRemaining counter. It sums Generation.credits for the current month and compares that to an allowance.
Allowance
export function monthlyAllowance(plan: string) {
return plan === "pro"
? Number(process.env.PRO_MONTHLY_CREDITS || 500)
: Number(process.env.FREE_MONTHLY_CREDITS || 20);
}
Only the string "pro" gets the higher number. That string is written by the Stripe webhook, described in synchronizing Stripe subscription state. Defaults are 500 and 20 when the env vars are empty. Number("") is 0, but the || falls through when the env var is unset or empty, so the defaults apply.
The month window
export async function creditsUsedThisMonth(userId: string) {
const start = new Date();
start.setDate(1);
start.setHours(0, 0, 0, 0);
const rows = await prisma.generation.findMany({
where: { userId, createdAt: { gte: start } }
});
return rows.reduce((sum, row) => sum + row.credits, 0);
}
The window is the server’s local month, starting at local midnight on the 1st. It is not a Stripe billing period and it is not UTC unless the host timezone is UTC. Say that in your UI if you show “resets monthly.” The query loads the month’s rows and adds credits in JavaScript. Each successful generation stores credits: 1, so the sum equals the number of finished generations.
Where to reject
const used = await creditsUsedThisMonth(user.id);
if (used >= allowance) {
return new Response("Monthly credit limit reached", { status: 402 });
}
Check before you call the model. The kit uses HTTP 402. The body is plain text, not JSON. A client should branch on the status, not on a typed error code.
The row is inserted only after the stream finishes, with credits: 1. Two requests that both pass the check before either insert can both run. The kit does not take a transaction or a row lock around the check. For a single-user demo that is fine. If you need a hard cap under parallel clicks, insert a pending ledger row inside a transaction before calling the provider, and mark it failed if the upstream request throws.
What a credit is not
The kit does not read token counts from the provider. One generation costs one credit regardless of prompt length, aside from the 4000-character rejection. There is no per-day cap and no team pool.
Want to skip the setup? The AI SaaS Starter already includes the monthly sum, the free and Pro allowances, and the 402 response.