Adding auth and billing to a SaaS is rarely limited by the integration itself. Better Auth's official Stripe plugin, Clerk's prebuilt identity layer and tiun's connected backend all get you to a working sign-up and checkout flow in hours rather than weeks. What consumes the schedule is everything downstream: dunning policy, disputes, grace periods, entitlement edge cases and tax. Plan for those and the route you choose matters far less than the scope you commit to.
You plan for two things: auth and billing. You build seven. The gap between that plan and reality is where most SaaS schedules fall apart.
This guide names all seven pieces, walks through three routes for handling them in Next.js, and gives honest estimates for each, including the edge cases that never show up in vendor tutorials but always show up in your sprint.
What you actually need to build
Most developers mentally sketch this as sign up, then pay. That mental model misses five pieces that will demand your time anyway.
The full list:
Identity: sign-up, sign-in, social logins, MFA, password reset, session management
Checkout: pricing page, payment form, trial handling, proration
Subscription state: a record, in your database, of what plan each user is on and when it changes
Entitlement checks: gating features based on that plan state, consistently, across every route and API endpoint
Webhook handling: receiving payment events from your provider, verifying their signatures, making your database match what the provider knows
Dunning: what happens when a card fails, covering retry logic, grace periods, access suspension and emails
Tax: at minimum, collecting and storing what you need; at most, automated tax through a service such as Stripe Tax
Identity and checkout get estimated. The other five get discovered.
The three routes
Route 1: assemble it yourself (Better Auth and Stripe)
This is the most flexible path and the one most often described inaccurately, in both directions.
Better Auth handles identity: sessions, social providers, 2FA, organisations. Stripe handles payments, subscriptions and webhooks, and knows nothing about your users beyond what you tell it.
The part most comparisons get wrong is the connecting code. Better Auth ships an official Stripe plugin, stable since version 1.3, and it covers more of the billing surface than you might expect. It creates a Stripe customer on signup and links it to the user record, manages plans and pricing, processes subscription lifecycle events, verifies webhook signatures, supports trials and upgrades, and handles seat-based team subscriptions. Plan limits are a config field on the plan object rather than code you write.
What stays yours is everything downstream of the happy path: dunning policy, dispute handling, grace periods, entitlement edge cases, and tax, which no auth library touches. You also end up with two accounts and two dashboards, your auth provider and Stripe, with no single place where customer, subscription and access state all agree. Reconciling those two views is an ongoing job of its own.
A note on Auth.js, since most guides still name it first. In September 2025 the Better Auth team took over its maintenance. It now receives security patches only, with no new feature development, and its own maintainers point new projects at Better Auth. If you are already running Auth.js v5, nothing breaks. Starting there today means building on a library that is no longer being developed.
On timelines: with the Stripe plugin doing the connecting work, getting auth and billing talking to each other is an afternoon, not a fortnight. What sets the schedule is scope and policy. A tightly scoped consumer build is a couple of days. A B2B build with team accounts, MFA and admin tooling runs to a week or more, and very little of that time goes on the Stripe integration itself. Estimates that put this route at two or three weeks are usually counting the policy work and attributing it to the plumbing.
The ongoing maintenance cost is real too. Every Stripe API version bump, every new social provider your users request, every security patch to Better Auth is yours to absorb, as is keeping the two dashboards in agreement.
Choose this route if you have unusual auth requirements, want fine-grained control over both systems, or want no production dependency on a third-party auth service, and you are comfortable running two systems of record.
What a basic setup looks like in Next.js
Install and configure Better Auth with your chosen providers and the Stripe plugin. Let the plugin create the Stripe customer on signup and store the stripeCustomerId against the user record. Define your plans, with their price IDs and limits, in the plugin config. When a user subscribes, the plugin creates the Checkout session and writes the subscription record when the webhook confirms it. On every protected route and API call, read from your database rather than from Stripe to check what plan the user is on.
That last point matters architecturally: keep plan state in your own database, not in your identity provider's metadata. Storing it in JWT claims or identity metadata creates race conditions on plan upgrades, costs you a transactional update on every change, and locks your data model to that provider.
Route 2: a provider that covers both (Clerk and Stripe)
Clerk covers a wider identity surface than almost anything else in this comparison. Prebuilt UI components, social logins, MFA, organisations, roles and permissions, and enterprise sign-in through SAML and OIDC are all first-party, and it connects to Stripe either through its own billing features or through your own Stripe setup. If enterprise customers are anywhere on your roadmap, this is the route where SSO is already present rather than something you bolt on in year two.
The time you save is identity code you never write, and Clerk's own webhooks are easier to reason about than raw Stripe events. The setup itself is comparable to route one; the difference shows up in everything you did not have to build around sign-in.
The trade-offs are lock-in and cost at scale. Clerk bills per monthly retained user, meaning someone who returns at least a day after signing up, so signups that never come back do not count against you. If you later need to migrate your identity layer, that is a non-trivial project. You are also still running two systems, Clerk and Stripe, with the same reconciliation question as route one.
Choose this route if you want the widest identity feature set for the least code, including enterprise sign-in and organisations, and the retained-user pricing fits your unit economics.
Route 3: a backend that arrives connected (tiun)
tiun is a backend service that ships with auth and billing already connected, with both feeding into one platform and one source of truth. You configure a system that is already integrated rather than assembling the pieces yourself, and you reconcile one dashboard rather than two.
There are two ways to integrate. The agent path is two steps: install the skill, then connect the MCP server. The server is no longer read-only, so products can be created directly from Claude Code, Cursor, Lovable and other agent environments, and the agent completes the integration without you copying IDs between tabs. The manual SDK path is documented at about an hour end to end, which we confirmed by running it ourselves before publishing this number.
The constraints are worth stating plainly. Sign-in covers email with a one-time passcode and social SSO. SAML exists, but is not yet offered for enterprise deployments. Organisation-level access controls, where your customer invites teammates and adds them to their plan, are on the roadmap rather than available today. tiun also does not handle enterprise invoicing. If you need any of those now, this is not the route for now, though they are roadmap gaps rather than a reason to plan a migration.
Choose this route if you are building a consumer or prosumer SaaS, you want auth and billing done in an afternoon, and enterprise sign-in and invoicing are not near-term requirements.
The edge cases that consume the schedule
This is the section vendor tutorials skip. Each of these will happen to your users. Each requires a deliberate decision, not just a provider setting.
A failed payment
A user's card is declined for insufficient funds. Stripe retries on your schedule, fails again, and your webhook receives invoice.payment_failed. What does your app do?
Card expiry is the example most guides reach for, and it is the weakest one. Stripe's card account updater refreshes many expiring cards before they ever fail, and hard declines are skipped by retry logic rather than retried. Insufficient funds is the case that actually reaches your dunning code.
If you have not handled it, the subscription lapses in Stripe while your database still shows the user as active. They keep accessing paid features. You lose the revenue and have no clean path to recovery without manually patching records.
You need a grace period policy, a dunning email sequence, and a clear state machine. The path runs from active to past_due. From there your Stripe retry settings decide the ending, and there are three options, not two: cancel the subscription, mark it unpaid, or leave it as is. The last two catch people out. A subscription marked unpaid stays in place while Stripe stops attempting payment, so the invoice that failed remains unpaid and later invoices are created and then closed automatically rather than chased. Leave it as is and the subscription simply sits at past_due indefinitely. In both cases, entitlement logic that only checks for canceled keeps serving a customer who has stopped paying.
Disputed charge
A user disputes a charge with their bank. Stripe sends a charge.dispute.created webhook. The disputed amount and a dispute received fee come out of your balance the moment the dispute opens, before anyone reviews it. If you win, the amount is returned but the received fee is not. Since June 2025 there is a second fee: countering a dispute adds a dispute countered fee, returned only if you win. Ignore the dispute and you lose by default.
You need to handle the webhook, flag the account in your system, and respond to the dispute with evidence before the deadline. Stripe's dashboard has tooling for this, but the webhook handling and account flagging are yours to build.
Plan switch mid-cycle
A user upgrades from a $29/month plan to a $99/month plan on day 15 of their billing cycle. What do they owe?
Stripe handles proration by default, but you need to decide whether you are using it, communicate it clearly to the user, and update the plan state in your database when the customer.subscription.updated webhook fires, rather than when the user clicks upgrade. The webhook is the source of truth. If you update your database on the button click and the webhook fails or arrives late, your access control is inconsistent with Stripe's state.
Cancellation
A user cancels. Do they lose access immediately, or at the end of the period they have paid for?
Both are defensible, but you need to implement the choice. Cancel at period end means Stripe sets cancel_at_period_end: true, the subscription stays active until the period ends, then sends customer.subscription.deleted.
One qualifier on that field. Stripe has deprecated cancel_at_period_end for subscriptions configured with flexible behaviour, meaning billing_mode set to flexible, and points those integrations at cancel_at with min_period_end or max_period_end instead. The behaviour is unchanged. If you are on classic billing mode, there is nothing to change and cancel_at_period_end is still the field to use.
Either way, your app needs to handle that final event to downgrade the user, and to avoid downgrading them the moment they hit cancel.
Abandoned signup
A user starts a trial, enters an email, and disappears. Or they complete signup but never enter payment details.
You now have a user record with no subscription. When they come back six months later and try to subscribe, does your system create a duplicate customer? Does the trial start from zero or from when they first signed up?
These are data integrity questions rather than billing questions. You need to handle them at the point where you create customers, and the answer is almost always: look up by email before creating, never create duplicates, store the customer ID before the checkout session completes.
What each route actually costs
Route | Time to a working integration | What stays on your plate |
|---|---|---|
Better Auth and Stripe | An afternoon for the connected happy path using the official Stripe plugin. A couple of days tightly scoped, a week or more with team accounts, MFA and admin tooling | Dunning, disputes, grace periods, entitlement edge cases, tax. Security patches and Stripe API version bumps. Two dashboards to reconcile |
Clerk and Stripe | Comparable setup, with less identity code to write. Enterprise sign-in and organisations arrive prebuilt | The same downstream billing logic. Stripe webhooks are still yours. Two dashboards, plus per-retained-user cost as you grow |
tiun | About an hour on the manual SDK path, less on the agent path | Less of it. Billing state and tax sit with the platform, in one dashboard. No organisation-level access controls or enterprise invoicing yet |
These estimates assume a developer who has built with these tools before, and comparable scope. First-time implementations run longer. The tiun figure is for a consumer or prosumer build with no organisations, so compare it against the tightly scoped row rather than the B2B one.
How to choose
What matters is what your product will need in year two, not which tools you prefer today.
If you expect enterprise customers who will require SSO, SAML or organisation-level permissions, build that path from the start. Clerk and Better Auth both cover it: Clerk as a managed service, Better Auth through its SSO plugin, which supports OIDC, OAuth2 and SAML 2.0, with SCIM available as a separate package. tiun does not cover enterprise sign-in or invoicing today.
If you are building for consumers or prosumers and want to validate quickly, the tiun path gets you to a working billing integration in an afternoon with one system to reconcile instead of two, and the Clerk path gets you there in days with more identity surface than you are likely to need yet.
The edge cases above are non-negotiable regardless of route. They are what billing looks like in production. The difference between routes is how much of that logic you write yourself, and how many systems have to agree on the answer.
Plan for the edge cases in your initial timeline and you will ship close to schedule. Discover them after launch and you will spend the first month patching instead of shipping.
Frequently asked questions
How long does it actually take to add auth and billing to a SaaS app?
Less than most estimates suggest if you count the integration, and more than most suggest once you count policy. Better Auth's official Stripe plugin handles customer creation, subscription records, webhook verification and plan limits, so connecting identity to billing is an afternoon rather than a fortnight. Clerk with Stripe is comparable, with less identity code to write. tiun, which ships with auth and billing already connected, takes about an hour on the manual SDK path. What consumes the schedule on every route is the same list: dunning, disputes, grace periods, entitlement edge cases and tax. Budget for those separately, and expect a first implementation to run longer than any of these figures.
Should I store subscription state in my own database or in Stripe?
Store it in your own database. Stripe is the financial source of truth, so reconcile against it whenever there is a discrepancy, but your app's access control should read from your own database rather than calling Stripe's API on every request. More importantly, write subscription state when the webhook fires, not when the user clicks a button. The webhook is the only reliable signal that Stripe's state has actually changed. If you update on the button click and the webhook arrives late or fails, your entitlement logic diverges from Stripe's records. Avoid storing plan state in JWT claims or identity metadata as well: it creates race conditions on plan upgrades and ties your data model to whichever provider issued the token.
What is the difference between Clerk and Better Auth for a Next.js SaaS?
Better Auth is open source and runs on your own infrastructure. You own the session logic, the user records and the credentials, nothing lives on a third-party server, and there is no per-user fee. Its SSO plugin supports OIDC, OAuth2 and SAML 2.0, with SCIM in a separate package, so enterprise sign-in is available without leaving the library. Clerk is a managed service: it hosts your identity layer, ships prebuilt UI components for sign-in and sign-up, and covers social providers, MFA, organisations, roles and enterprise connections without custom code. Clerk bills per monthly retained user, meaning a user who returns at least 24 hours after signing up, and moving off it later is a migration project. Better Auth costs nothing per user, but you absorb every security patch and API change yourself. Both can meet enterprise requirements, so the real question is how much identity code you want to own rather than what each one is capable of.
Which Stripe webhook events do I need to handle for a subscription-based SaaS?
Five events cover the core subscription lifecycle. checkout.session.completed fires when a user subscribes, so use it to write the subscription record and provision access. invoice.payment_failed fires when a charge fails, which is where your dunning sequence starts. customer.subscription.updated fires on plan changes and renewals, so sync the new plan state. customer.subscription.deleted fires when a subscription fully ends, so downgrade the user's access. charge.dispute.created fires when a user initiates a chargeback, so flag the account and respond with evidence before Stripe's deadline. Verify the webhook signature on every incoming event. Beyond these five, customer.subscription.trial_will_end is worth handling if you offer trials, since it fires 72 hours before the trial converts and gives you a window to prompt for payment details.