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.

2 min read

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.

Product updates for developers shipping with Designyff kits.

Purchased source may be used in personal, commercial, and client projects. Do not redistribute or resell the original source. Terms of Service

Designyff © 2026 | Starter kits for developers | Powered by  Tacko