Better Auth: key to solve authentication Problems

Joshua D'Costa
Growth & Marketing
Dec 3, 2025
|
5
min
If you’ve ever found yourself juggling separate authentication logic, billing workflows, and subscription handling, you’re not alone. Most developers don’t wake up excited to wire together auth flows, payment events, and usage tracking, especially when that time could go toward building the actual product. That’s why many teams lean on libraries and providers to offload the heavy lifting.
Over the years, tools like NextAuth (Auth.js), Auth0, and Clerk have become the default choices. They work well for many cases, but each brings its own set of compromises, whether it’s complexity, pricing, limited flexibility, or concerns around vendor lock-in.
Modern, open-source ecosystems like Better Auth take a fresh approach by giving developers a TypeScript-native, fully customizable authentication layer without the usual overhead. It aims to solve the problems that older solutions haven’t fully addressed, especially when you're building products that need clean, unified flows for signup, subscriptions, metering, and webhooks.
Let’s understand what Better Auth offers, where it fits in the current auth landscape, and how you can integrate it smoothly into your stack.
What is Better Auth?
Better Auth is a headless, framework-agnostic authentication and user-management framework with a strong focus on developer experience. In practical terms, it provides the core auth flows (signup, login, sessions, etc.) out of the box and uses a plugin architecture so you can add on extra functionality. Better Auth itself doesn’t handle payments – it just gives you a robust, type-safe auth layer you can drop into any JS/TS stack.
When you install Dodo Payments’ Better Auth adapter, your auth system becomes “payments-aware.” For example, the adapter will automatically create a Dodo Payments customer behind the scenes whenever someone signs up, handle your checkout sessions using defined product slugs, wire up a self-service billing portal, and even ingest metered usage events for usage-based billing. It also validates and parses Dodo Payments webhook events for you. All of this is exposed through a unified, TypeScript-typed API, so you don’t have to manually sync up your auth database with Dodo Payments’ SDK or write your own webhook handlers.
Better Auth Features
Here are some of the key features you get immediately by using Dodo Payments’ Better Auth plugin:
Automatic customer creation on signup: As soon as a user registers in Better Auth, the adapter creates a corresponding customer behind the scenes (no extra API calls needed).
Type-safe checkout flows (product slug mapping): You can define products by slug (e.g.
"premium-plan") instead of hardcoded IDs.
At checkout, Better Auth ensures the correct product is used, and the TypeScript types catch mismatches early.Embedded billing portal: Your users can access their customer portal for updating cards, viewing invoices, managing subscriptions, etc. directly in your app after login, with minimal effort.
Built-in webhook handling: The adapter automatically sets up a webhook endpoint that verifies signatures. It fires typed handlers for payments, subscriptions, refunds, and more, so you don’t have to write your own signature-checking logic.
Metered usage ingestion: If you have usage-based billing, you get endpoints to ingest usage events and list usage records. This lets you bill for API calls, hours, seats, etc., without extra work.
Full TypeScript support: From your frontend calls to your server logic, everything is fully typed. This end-to-end type safety means fewer runtime errors and clearer code.
(Extra) Observability and logging: The adapter surfaces decline reasons, transaction details, and telemetry from Dodo Payments’ API, so you can log or display debugging info as needed.
Installation & Quick Setup
Prerequisites
Before you begin, make sure you have:
Node.js 20+ (to use the latest libraries).
A Dodo Payments account with your API keys on hand (you’ll need the secret key and to set up webhook secrets).
An existing project using Better Auth (or at least the Better Auth core installed).
You’ll also need to generate a Better Auth secret key (via the Better Auth CLI or docs) and set it in your environment. Better Auth requires a couple of env vars, for example:
BETTER_AUTH_SECRET=your-better-auth-secret
BETTER_AUTH_URL=http://localhost:3000
Better Auth auto-detects these, so once they’re in your .env you don’t have to reference them in code. (And as always, never commit secrets – use .env or a secret manager.)
Install packages
Run the following in your project root to install the Dodo Payments adapter and its deps:
npm install @dodopayments/better-auth dodopayments better-auth zod
This pulls in:
@dodopayments/better-auth(the Better Auth plugin),dodopayments(the Dodo Payments SDK),better-auth(if not already installed),zod(for schemas).
Dodo Payments’ docs confirm this setup step.
Environment variables
Add the following to your .env (or equivalent) file:
DODO_PAYMENTS_API_KEY=your_api_key_here
DODO_PAYMENTS_WEBHOOK_SECRET=your_webhook_secret_here
BETTER_AUTH_URL=http://localhost:3000
BETTER_AUTH_SECRET=your_better_auth_secret_here
The first two are for Dodo Payments: your API key (used by the SDK client) and the webhook secret (used to verify incoming webhooks). The last two are for Better Auth itself (its base URL and secret). As noted, never commit these values to version control. The Better Auth docs also show setting these variables in .env.
Server-Side Integration (Core Wiring)
On the server, you need to wire up Better Auth with the Dodo Payments plugin. In a file like src/lib/auth.ts (or wherever you configure Better Auth), do something like this:
import { BetterAuth } from "better-auth";
import { dodopayments, checkout, portal, webhooks, usage } from "@dodopayments/better-auth";
import DodoPayments from "dodopayments";
const dodoPayments = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY!,
environment: "test_mode", // use "live_mode" in production
});
export const { auth, endpoints, client } = BetterAuth({
plugins: [
dodopayments({
client: dodoPayments,
createCustomerOnSignUp: true,
use: [
checkout({
products: [{ productId: "pdt_xxx", slug: "premium-plan" }],
successUrl: "/dashboard/success",
authenticatedUsersOnly: true,
}),
portal(),
webhooks({
webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_SECRET!,
// you can also specify onPaymentSucceeded, onPaymentFailed, etc.
onPayload: async (payload) => {
console.log("Received Dodo webhook:", payload.event_type);
},
}),
usage(),
],
}),
],
});
Here’s what this does:
We create a
DodoPaymentsclient with your API key. (Note: use"test_mode"locally and switch to"live_mode"in production.)In
BetterAuth({...}), we include one plugin:dodopayments({...}).We pass in the client, enable
createCustomerOnSignUp, and list our sub-plugins inuse: [...].The
checkout()plugin maps product IDs to slugs (so you can use friendly slugs in code), plus a redirect URL.The
portal()plugin enables the customer portal.The
webhooks()plugin takes your webhook secret and handlers.The
usage()plugin enables usage ingestion endpoints.
This example is a minimal wiring. You can expand it with more products, custom webhook event handlers, etc. (The official docs have a similar sample.) Remember to keep your environment in test_mode while developing, then flip to "live_mode" when you go live.
Client-Side Setup
In your frontend (e.g. React, Next.js, etc.), set up the Better Auth client and include Dodo Payments’ client plugin. For example, in src/lib/auth-client.ts:
import { createAuthClient } from "better-auth/react";
import { dodopaymentsClient } from "@dodopayments/better-auth";
export const authClient = createAuthClient({
baseURL: process.env.BETTER_AUTH_URL || "http://localhost:3000",
plugins: [dodopaymentsClient()],
});
This gives you authClient.dodopayments.* methods on the client. (Make sure BETTER_AUTH_URL is set to your API base, like http://localhost:3000.) Now you can call the Dodo Payments-related methods from your UI and they’ll hit your server’s Better Auth endpoints.
Core Usage Examples
Once everything is wired up, here are some common usage examples:
Create a Checkout Session (by slug): This is the recommended way to start a checkout. For example:
const { data: session } = await authClient.dodopayments.checkoutSession({
slug: "premium-plan",
referenceId: "order_123",
});
if (session) window.location.href = session.url;
This will open the checkout page for your “premium-plan” product. You don’t need to collect card details yourself – Dodo Payments handles it.
Note: There is also an older authClient.dodopayments.checkout() method for backwards compatibility, but for new integrations prefer checkoutSession with slugs or product carts.
Customer Portal: To give users access to their billing portal, do:
const { data: portal } = await authClient.dodopayments.customer.portal();
if (portal?.redirect) {
window.location.href = portal.url;
}
This redirects the user into Dodo Payments’ self-service portal, where they can manage their subscriptions and payment methods.
List Subscriptions / Payments: You can list the customer’s subscriptions or payments via:
const { data: subs } = await authClient.dodopayments.customer.subscriptions.list({
query: { limit: 10, page: 1, status: "active" },
});
const { data: payments } = await authClient.dodopayments.customer.payments.list({
query: { limit: 10, page: 1, status: "succeeded" },
});
(Adjust the query params as needed.) This lets you display billing history in your app.
Metered Usage (Usage plugin): For usage-based plans, record events like:
await authClient.dodopayments.usage.ingest({
event_id: crypto.randomUUID(),
event_name: "api_request",
metadata: { route: "/reports", method: "GET" },
timestamp: new Date(),
});
Dodo will tally this usage behind the scenes. You can also list recent usage for a meter:
const { data: usage } = await authClient.dodopayments.usage.meters.list({
query: { page_size: 20, meter_id: "mtr_yourMeterId" },
});
(Note: The plugin enforces a time window on the
timestamp, so events too far in the past/future may be rejected.)
Webhooks (Secure Handling)
The Dodo Payments plugin automates webhook handling for you. By default it exposes an endpoint at /api/auth/dodopayments/webhooks (in Next.js, that’s your API route). To use it, do the following:
Set up the webhook URL in Dodo’s dashboard. Use your domain plus
/api/auth/dodopayments/webhooks.Configure the webhook secret. Save the secret from Dodo (when you create the webhook) into your
.envasDODO_PAYMENTS_WEBHOOK_SECRET.Enable the plugin in Better Auth. We already added
webhooks({ webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_SECRET!, ... })in the server setup above.
With that in place, incoming webhook events will be verified against your secret and parsed. You can provide handler callbacks in the webhooks() config. For example:
webhooks({
webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_SECRET!,
onPaymentSucceeded: async (payload) => { /* handle successful payment */ },
onPaymentFailed: async (payload) => { /* handle payment failure */ },
onSubscriptionCancelled: async (payload) => { /* ... */ },
// ...and so on for disputes, refunds, etc.
})
The plugin’s signature verification means you can trust the payload. Webhooks plugin processes real-time events from Dodo Payments with secure signature verification.
Configuration Reference
Here’s a quick summary of the main plugin configs:
dodopayments({ client, createCustomerOnSignUp, use: [...] }): The top-level plugin. Pass your Dodo Paymentsclient, setcreateCustomerOnSignUp: trueto enable auto-creating customers, and list the sub-plugins inuse.checkout({ products, successUrl, authenticatedUsersOnly }): Inuse, this plugin maps your Dodo Payments product IDs to slugs. You provide an array of{ productId, slug }, asuccessUrlfor redirects, and whether customers must be logged in.portal(): Enables the customer portal. No additional options needed.usage(): Turns on usage metering ingestion and listing.webhooks({ webhookKey, onPayload, ...handlers }): Enables webhook handling. Provide thewebhookKeyfrom your Dodo Payments dashboard, and then specify callbacks likeonPaymentSucceeded,onSubscriptionCancelled, etc. You can also use a genericonPayloadhandler.
Troubleshooting & Best Practices
Invalid API key: Double-check that
DODO_PAYMENTS_API_KEYis correct and has the right scopes. A bad key will cause authentication errors on every call.
Webhook signature errors: Ensure
DODO_PAYMENTS_WEBHOOK_SECRETmatches exactly what you set in the Dodo Payments dashboard for your webhook URL.Customer not created: If you don’t see a customer in Dodo Payments after signup, make sure
createCustomerOnSignUp: trueis set.
Best practices:
Keep all keys and secrets in environment variables or a secrets manager.
Test everything in test_mode before going live, and monitor webhooks/events in the dashboard. Log payment failures and decline reasons on your end for debugging.
Use idempotency keys on your requests if you retry anything. If you’re migrating from another billing system, consider rolling out the new integration to a subset of users first to ensure it’s working smoothly.
Conclusion
If you want a single, predictable integration that handles auth, subscriptions, usage billing, and webhooks with TypeScript-first ergonomics, Dodo Payments’ Better Auth adapter is an efficient choice. It bundles together all the typical payment plumbing (customer creation, checkouts, portal, usage reports, etc.) into a unified flow.
That means you can spend less time writing custom glue code and more time on your product.




