Comparisons
Comparison

The best way to add billing to a Next.js app depends on where entitlements live

Four ways to add billing to a Next.js app, compared on what you still have to build after checkout works: entitlements, quota enforcement and seats.

Dharmendra Jagodana10 min read

In short

Four ways to add billing to a Next.js app: wire Stripe yourself, start from a template, bundle it with your auth provider, or use a platform that enforces limits. The transaction fee is not the deciding variable. Where entitlement state lives is, because a webhook-fed table is stale right when a customer upgrades.

Every guide to adding billing to a Next.js app finishes in the same place. Checkout works, the webhook fires, a row in your database flips to active, and the tutorial ends. That is the easy half. The half that shapes your codebase starts on the very next request, when some route handler has to decide whether this workspace is allowed to do the thing it just asked for.

The one-paragraph answer

If your pricing is one flat amount per interval and you want money moving this week, take nextjs/saas-starter and stop reading. Already on Clerk for auth? Clerk Billing bolts plans and per-seat pricing onto gating you have already written. If you sell into the EU and tax is the thing keeping you awake, wire Stripe yourself and add Stripe Tax, because nothing bundled comes close to its coverage. And if what you need is a limit that refuses a request rather than a flag that permits one, none of those three do that. Comparing them on transaction fee will never tell you so.

Four routes, and what each one leaves you holding

Wire Stripe yourself. A Checkout session, a webhook handler, a subscriptions table, and a hasPro check that ends up written three times: once in middleware.ts, once in a server component, once in the route handler you forgot. Stripe has since added Entitlements, which removes some of that bookkeeping: when a subscription becomes active, Stripe creates an active entitlement for each feature attached to the product. Read the object before you plan around it. Its fields are id, object, feature, lookup_key and livemode. No quantity, no counter, no balance. Provisioning is by webhook plus a list API, which Stripe's own guidance describes as the way to execute feature provisioning.

Start from a template. The current one is nextjs/saas-starter, which replaced Vercel's nextjs-subscription-payments when that repo was sunset. Check which you are looking at, because the old one still ranks. The new template gives you a pricing page wired to Stripe Checkout, subscription management through the Stripe Customer Portal, Owner and Member roles, and a webhook handling two subscription events: customer.subscription.updated and customer.subscription.deleted. Its README calls it "intentionally minimal and to be used as a learning resource", which is the most honest sentence in this entire category. The template it replaced was blunter about where the ceiling is: Stripe Checkout "currently supports pricing that bills a predefined amount at a specific interval", and "more complex plans (e.g., different pricing tiers or seats) are not yet supported". Lemon Squeezy's nextjs-billing sits in the same place, handling subscription_created and subscription_updated, which its README says are "enough to get a billing system in place". Both are good starting points. Neither has a concept of a limit.

Bundle it with your auth provider. Clerk Billing adds plans, per-seat pricing with seat limits, a base fee and free trials, and gates on them with the same has() helper and <Show /> component you already use for roles. Clerk's docs describe that helper as checking whether the user has been granted a role, permission, feature or plan, and returning a boolean. Which is the point: it is the Stripe Entitlements shape with a nicer developer experience on top. One thing to be clear about, because the naming invites the mistake: Clerk Billing is its own product, not a wrapper over Stripe Billing. Clerk's docs say plans and subscriptions created in Clerk are not synced to Stripe, which is only used for payment processing. Clerk charges 0.7% of billing volume for it, on top of Stripe's processing fees paid directly to Stripe.

Use something that holds the counter. This is where BuildBase sits, and it is worth being precise about what that means, because we are not an alternative to Stripe. The billing module runs on your own Stripe account. What it adds is the piece none of the three routes above ship: bb.usage.record(workspaceId, { quotaSlug: 'api-calls', quantity: 1 }) returns the remaining balance in the same response, and on a plan without overage enabled the call fails rather than recording the increment. One await, inside the request that has to answer. Metering and enforcement are one operation, which is the only arrangement in which they cannot drift apart.

An entitlement is a boolean. A quota is a counter.

These are two different questions and the industry has quietly agreed to only answer one of them.

"Is this customer on Pro" is a boolean. It changes a handful of times in a customer's life, it is cheap to cache, and a webhook is a perfectly reasonable way to learn about it. Stripe Entitlements answers it. Clerk's has() answers it. Every hasPro check in every SaaS codebase answers it. It is worth being clear that this boolean is not a feature flag, even where the same component renders both, because the two have different owners and fail in opposite directions.

"Has this workspace used its 10,000 API calls" is a counter. It changes on every request, it is worthless the moment it is stale, and no webhook will ever deliver it to you in time. Stripe is explicit that its meters are eventually consistent: meter events are processed asynchronously and may not be immediately reflected in aggregates, and meter event summaries are described in the API reference as an eventually consistent view of reported usage. That is the correct design for producing an accurate invoice. It is the wrong design for a route handler deciding, right now, whether to return a 429.

Key takeaway

Every billing provider will answer "is this customer entitled". Almost none of them will answer "how much do they have left" in time to act on it. If your pricing has a limit in it, that gap is your code, whichever provider you pick.

So the real question for a Next.js codebase is not which provider takes the money. It is whether a counter lives somewhere your route handler can read and decrement in one round trip, or whether you are going to build that yourself on top of a table a webhook fills in.

Where Next.js makes the staleness visible

Here is the bug, and it always arrives the same way. A customer upgrades. Stripe redirects them back to your success page. That page is a server component, so it renders on the server, reads your database, and finds the row the webhook has not written yet. Your happiest customer of the week sees the free plan.

You can fix it, and the fixes are all fine: read the subscription from Stripe directly on the success path instead of from your own table, or render that page from a pending state and let the client poll, or make the success route the one place that does not trust the cache. The first of those is what nextjs/saas-starter quietly does, and it is the best thing in the template: its Checkout success_url points at a route that fetches the session and writes the subscription before the page renders, so the happy path never waits on a webhook at all. What you cannot do is not decide. Every route in your app that reads plan state from a webhook-fed table has a window where it is wrong, and the App Router makes that window most visible on exactly the page a customer is most excited to be looking at.

I should be straight about where we have a version of the same problem. The per-metric quota check described above is synchronous and has no webhook in it, so it does not have this window. Our org-level check, the separate question of whether an organization is entitled to the platform at all, is a different code path with a short-lived cache in front of it, and it is not instant after a payment lands. Two questions, two mechanisms. Only one of them sits in the request path with nothing cached in front of it, and anyone claiming their billing state is globally instant is describing a design they have not shipped.

FeatureBuildBaseStripe
Entitlement shapeCounter with a remaining balanceBoolean feature grant, no quantity field
Delivery to your appRead and decremented in the requestWebhook plus a list API
Behavior at the limitFails unless overage is on; trials always capMeters it; the block is yours to write
Tax determinationRecords tax on the invoice, does not calculate itStripe Tax, calculated and collected
Languages and frameworksNext.js, Vite, React Router / Remix, TanStack Start, Express, NestJS, Fastify, Hono, AstroAny language with an HTTP client
Scope20 modules including auth, workspaces and RBACPayments and billing

Where Stripe wins

Stripe Tax is not a feature we have a smaller version of. It is a product we do not have at all. Stripe calculates and collects tax across the EU, the UK, Norway, Switzerland, every US state, Australia, Japan, Singapore, the UAE and more; our billing module records a tax figure on an invoice, which is a different job entirely. To be precise rather than dramatic about it: because checkout runs on your own Stripe account, you can switch Stripe Tax on there and we will pass it through, so the honest version is that we have no tax engine of our own, not that using us costs you Stripe Tax. Revenue Recognition is the same story for accrual-basis reporting. If you are selling into the EU and cannot answer a VAT question, that decides it, and it decides it against us. Stripe is also language-agnostic. Our SDK covers six JavaScript frameworks, so a Django or Rails team can use Stripe Billing today and cannot use BuildBase at all.

How to actually decide

Write down your pricing page first, then read it back as questions your code has to answer. If every line is a flat monthly amount, all four routes work and you should take the fastest one, which is the template. If a line says "up to 10,000 API calls", you have a counter, and you should decide now where it lives rather than discovering in month four that it lives in four places. Decide at the same time whether that counter is an allowance that resets or a balance the customer bought, because the two carry different behavior at renewal. If a line says "per seat", you need a seat count checked against a plan limit on every invite, which is app-layer work no matter who processes the card. And if a line mentions VAT, start from Stripe Tax and fit the rest around it.

Our own pricing sits at $49 a month for Launch, with 25,000 MAU included, and there is no free plan, though every plan carries a 7-day trial that does not ask for a card. Storage is the only quota that bills past its limit, at $0.10 per GB. Everything else hard-stops, which is the whole argument above applied to ourselves. Card details are handled entirely by Stripe and never stored on our servers.

Once you have picked a route, the implementation posts are the next stop: charging per seat in Next.js covers the invite-button case in code, and charging per API call in Next.js covers the meter-and-enforce loop in a single route handler. If the question underneath all this was really which metering vendor to buy, best usage-based billing tools argues that one on its own terms. And if your pricing page opens with a trial rather than a plan, running one without a credit card is the case where the entitlement check has to admit a status that is not active.

A note on sourcing. Stripe's Entitlements object shape, the webhook-plus-list-API provisioning guidance and the eventual consistency of meters all come from Stripe's own documentation, checked on 9 September 2026. The nextjs/saas-starter, nextjs-subscription-payments and nextjs-billing quotes were read directly from each repository on the same day, as were the Clerk lines, from Clerk's own docs and pricing page. No Stripe Billing rate appears above, and that is deliberate: the figure could not be read off Stripe's pricing page today, and Clerk's 0.7% is Clerk's own fee for a separate product rather than a second reading of Stripe's. Base Stripe processing fees and Clerk's plan pricing are absent for the same reason. A stale fee is worse than no fee. BuildBase figures come from PLANS and PLAN_FACTS in packages/shared/src/constants/platform-stats.ts, verified 9 September 2026 against the published plans, and the enforcement behavior from the metered-usage service in server/src/payment/.

billing
nextjs
comparison
alternatives

Frequently Asked Questions

What is the fastest way to add billing to a Next.js app?

A template. `nextjs/saas-starter`, which replaced Vercel's sunset `nextjs-subscription-payments`, gets Stripe Checkout, two subscription webhooks and the customer portal running in an afternoon. Its README calls it intentionally minimal, and it ends where per-seat or usage pricing begins.

Do Stripe Entitlements handle usage limits?

No. An active entitlement is a boolean feature grant with no quantity field, delivered by webhook. Counting usage, enforcing the limit and deciding what happens at zero all stay in your app.

Why does a customer still see the old plan right after upgrading?

Because the subscription webhook arrives after the redirect back to your app, and until it lands your database still holds the previous entitlement. Either read the subscription directly on the success path or gate that page on a pending state.

Do I need a billing vendor to charge per seat in Next.js?

No, but you need somewhere to hold seat count against plan limit and to answer that question on every invite. That check is app-layer work regardless of which provider takes the money.

Try BuildBase instead of Stripe

Start a trial and see how far you get in an afternoon. 7-day free trial, no credit card.

7-day free trialNo credit card requiredCancel anytime