Better Auth: key to solve authentication Problems

Joshua D'Costa

Growth & Marketing

Dec 3, 2025

|

5

min

better-auth-dodo-payments
better-auth-dodo-payments
better-auth-dodo-payments

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 DodoPayments client 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 in use: [...].

  • 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:

  1. Set up the webhook URL in Dodo’s dashboard. Use your domain plus /api/auth/dodopayments/webhooks.

  2. Configure the webhook secret. Save the secret from Dodo (when you create the webhook) into your .env as DODO_PAYMENTS_WEBHOOK_SECRET.

  3. 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 Payments client, set createCustomerOnSignUp: true to enable auto-creating customers, and list the sub-plugins in use.

  • checkout({ products, successUrl, authenticatedUsersOnly }): In use, this plugin maps your Dodo Payments product IDs to slugs. You provide an array of { productId, slug }, a successUrl for 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 the webhookKey from your Dodo Payments dashboard, and then specify callbacks like onPaymentSucceeded, onSubscriptionCancelled, etc. You can also use a generic onPayload handler.

Troubleshooting & Best Practices

  • Invalid API key: Double-check that DODO_PAYMENTS_API_KEY is correct and has the right scopes. A bad key will cause authentication errors on every call.

  • Webhook signature errors: Ensure DODO_PAYMENTS_WEBHOOK_SECRET matches 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: true is 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.

Scale your business with frictionless global transactions

Share It On: