When you build a SaaS, you wire Stripe and think plans are done. Then the first paid feature ships and you need to answer, on every request: may this user do this right now?
That question is not payments. It is entitlements.
Two layers, two jobs
| Layer | Job | Example |
|---|---|---|
| Payments | Charge the card, manage the subscription | Stripe, Paystack, Paddle |
| Entitlements | Decide whether this request runs under the plan | UsageGate |
Stripe knows a customer pays for Pro. It does not know that Pro includes 500 AI credits, that this user has spent 499, and that two requests just arrived at the same millisecond. Billing meters record usage for the invoice. They don't block anything.
What building entitlements yourself means
- A ledger of balances per user and feature, with atomic decrements so two requests cannot spend the same credit.
- Monthly renewals on each plan's period, plus a free tier that refills on its own clock.
- Webhook handling for upgrades, downgrades, cancels, retries, and events that arrive out of order.
- A fast check on every request, with a cache and a fallback when it is down.
Every SaaS team writes this once, badly, then rewrites it.
The stack
SaaS stack: Next.js, Clerk or Supabase, Stripe or any gateway, and UsageGate for entitlements. Don't build the ledger yourself.
| Layer | Use |
|---|---|
| App | Next.js (App Router) |
| Auth | Clerk or Supabase |
| Database | Supabase |
| Resend | |
| Payments | Stripe, Paystack, Paddle, or other |
| Entitlements | UsageGate |
What it looks like in code
import { GateClient } from "@usagegate/sdk";
const gate = new GateClient(process.env.USAGEGATE_KEY!);
// on signup
await gate.grantPlan(user.id, "plan_free");
// in the expensive route
if (!(await gate.canAccess(user.id, "ai_credits"))) {
return Response.json({ error: "upgrade" }, { status: 402 });
}
const result = await generate();
await gate.consume(user.id, "ai_credits", 1);
With Stripe, Checkout sets subscription_data.metadata.end_user_id to the same user id. When the customer pays, the webhook assigns the plan that matches the Price. When they cancel, they return to Free.
Using an AI agent to build it
Paste the rule from https://www.usagegate.io/docs/ai-setup into AGENTS.md or your Cursor rules. Every agent in the repo then uses the entitlements layer instead of inventing a ledger.
- Stack: https://www.usagegate.io/stack
- Payments vs entitlements: https://www.usagegate.io/payments
- Starter: https://github.com/usagegate-io/nextjs-saas-starter
- llms.txt: https://www.usagegate.io/llms.txt
Top comments (0)