More Tutorials

Integrate Better Auth with Chargebee

Better Auth
Webhooks

The Chargebee plugin for Better Auth connects Chargebee subscription billing with your auth layer. Because payments and identity often belong together, the plugin handles customer creation, subscription flows, and webhook processing so you can focus on product logic.

This tutorial walks through installation, server and client configuration, database migration, and webhook setup.

What you get

Prerequisites

Before you begin, make sure you have:

  • A working Better Auth setup with a configured database adapter
  • A Chargebee site with API access, including:
  • A Chargebee product catalog configured with item prices. Item prices should be referenced by ID in the plugin configuration (for example, pro-USD-Monthly).

Steps to Integrate Better Auth with Chargebee

Step 1: Install packages

Install the Better Auth Chargebee plugin:

npm install @chargebee/better-auth

If your repo splits client and server code, add the package in both places.

Install the Chargebee Node SDK on the server only:

npm install chargebee

Step 2: Configure the server (auth.ts)

Create a Chargebee client with your site and API key, then pass it into the chargebee plugin inside betterAuth.

import { betterAuth } from "better-auth"
import { chargebee } from "@chargebee/better-auth"
import Chargebee from "chargebee"

const chargebeeClient = new Chargebee({
  apiKey: process.env.CHARGEBEE_API_KEY!,
  site: process.env.CHARGEBEE_SITE!,
})

export const auth = betterAuth({
  // ... your existing config
  plugins: [
    chargebee({
      chargebeeClient,
      createCustomerOnSignUp: true,
      webhookUsername: process.env.CHARGEBEE_WEBHOOK_USERNAME,
      webhookPassword: process.env.CHARGEBEE_WEBHOOK_PASSWORD,
    }),
  ],
})

Set createCustomerOnSignUp: true if every new user should get a linked Chargebee customer. Use webhookUsername and webhookPassword in production so Chargebee can authenticate webhook requests with Basic Auth.

Environment variables (server)

Define these variables in your server environment (for example .env for local development and your host’s secrets store in production) so the Chargebee client can call the API and the webhook endpoint can verify incoming requests.

VariablePurpose
CHARGEBEE_API_KEYFull-access API key for your Chargebee site
CHARGEBEE_SITEYour Chargebee site subdomain (see Sites)
CHARGEBEE_WEBHOOK_USERNAMEBasic Auth username for webhooks (recommended in production)
CHARGEBEE_WEBHOOK_PASSWORDBasic Auth password for webhooks

Step 3: Configure the client (auth-client.ts)

Register the client plugin so the browser can call subscription helpers.

import { createAuthClient } from "better-auth/client"
import { chargebeeClient } from "@chargebee/better-auth/client"

export const authClient = createAuthClient({
  // ... your existing config
  plugins: [
    chargebeeClient({
      subscription: true, // set false if you only need customer linking
    }),
  ],
})

Step 4: Migrate the database

The plugin adds tables and fields for customers and subscriptions. Apply schema changes using Better Auth's CLI:

npx auth migrate

Or generate artifacts only:

npx auth generate

If you manage SQL by hand, align with the schema section below and the Better Auth database docs.

Step 5: Configure Chargebee webhooks

In the Chargebee dashboard, create a webhook endpoint that points to:

https://your-domain.com/api/auth/chargebee/webhook

/api/auth is the default base path for the Better Auth server; adjust if you changed it.

Subscribe at least to these events (see event types in the API reference):

If you configured webhookUsername and webhookPassword, enter the same credentials under Basic Authentication in the Chargebee webhook settings.

Enable subscriptions and define plans

Customer-only mode works without subscription configuration. To sell subscription plans, enable the subscription option and define plans using Chargebee item price IDs from your product catalog.

Static Configuration Example

Use a static configuration if your plans are defined directly in code:

chargebee({
  chargebeeClient,
  subscription: {
    enabled: true,
    plans: [
      {
        name: "starter",
        itemPriceId: "starter-USD-Monthly",
        type: "plan",
        limits: { projects: 5, storage: 10 },
      },
      {
        name: "pro",
        itemPriceId: "pro-USD-Monthly",
        type: "plan",
        limits: { projects: 20, storage: 50 },
        freeTrial: { days: 14 },
      },
    ],
  },
})

Dynamic Configuration (Recommended)

For better maintainability, load plans from your database. This keeps pricing IDs and marketing-related data out of source control.

subscription: {
  enabled: true,
  plans: async () => {
    const plans = await db.query("SELECT * FROM plans")
    return plans.map((plan) => ({
      name: plan.name,
      itemPriceId: plan.chargebee_item_price_id,
      type: "plan" as const,
      limits: JSON.parse(plan.limits),
    }))
  },
},

Common client flows

Create a Subscription (Hosted Checkout)

This opens a Chargebee-hosted checkout page. After completion, the user is redirected to an internal success URL and then to your specified successUrl.

await authClient.subscription.create({
  itemPriceId: "pro-USD-Monthly",
  successUrl: "/dashboard",
  cancelUrl: "/pricing",
})

List Active Subscriptions

Retrieve all active subscriptions for the current user:

const { data } = await authClient.subscription.list()

For an organization:

const { data: orgSubscriptions } = await authClient.subscription.list({
  query: {
    referenceId: "org_123",
    customerType: "organization",
  },
})

Change Plans

If the user already has an active subscription, use subscription.update. In some cases, you may need to provide a subscriptionId.

Attempting to create a new subscription while one is already active will result in an ALREADY_SUBSCRIBED error.

await authClient.subscription.update({
  itemPriceId: "enterprise-USD-Monthly",
  successUrl: "/dashboard",
  cancelUrl: "/pricing",
})

Billing Portal

Redirect users to the Chargebee billing portal to manage their subscription:

await authClient.subscription.portal({
  returnUrl: "/account/billing",
  fetchOptions: {
    onSuccess: (ctx) => {
      window.location.href = ctx.data.url
    },
  },
})

Organization Billing

To manage subscriptions at the organization level, use the organization plugin alongside the Chargebee plugin.

Enable Organization Support

Enable organization support in your Chargebee plugin configuration:

organization: { enabled: true }

Customer Customization (Optional)

You can customize how Chargebee customers are created and handled by using lifecycle hooks and parameter overrides.

Run Logic After Customer Creation

Use onCustomerCreate to execute custom logic after a customer is created in Chargebee:

chargebee({
  // ...
  onCustomerCreate: async ({ chargebeeCustomer, user }) => {
    console.log(`Customer ${chargebeeCustomer.id} for user ${user.id}`)
  },
})

Customize Customer Creation Parameters

Use getCustomerCreateParams to modify the data sent to Chargebee when creating a customer.

For example, splitting a full name into first_name and last_name:

getCustomerCreateParams: (user) => {
  const [firstName, ...rest] = (user.name ?? "").split(" ")
  return {
    first_name: firstName || undefined,
    last_name: rest.join(" ") || undefined,
  }
},

Organization Support

If you are working with organizations, use organization.getCustomerCreateParams instead of the user-level configuration.

Database Schema Overview

The plugin extends your database schema with additional fields and tables to support billing and subscriptions.

Updated Models

  • user
    Includes an optional chargebeeCustomerId. This may be omitted when using organization-only billing with organization customer mode.

  • organization
    Includes an optional chargebeeCustomerId when organization support is enabled.

  • subscription
    Stores subscription-level data, including:

    • Reference ID (user or organization)
    • Chargebee subscription and customer IDs
    • Status
    • Billing period and trial dates
    • Seat count
    • Metadata
  • subscriptionItem
    Stores individual line items associated with a subscription, such as:

    • Plans
    • Addons
    • One-time charges
      Includes quantity and pricing details.

Keeping Schema in Sync

After making changes to the plugin configuration, regenerate your database schema:

npx auth generate

Troubleshooting

SymptomWhat to Check
Webhooks ignoredEnsure the webhook URL matches your auth base path; verify selected events in Chargebee; confirm Basic Auth credentials match your environment configuration
Status out of dateConfirm webhooks are reaching your server; check that chargebeeCustomerId and chargebeeSubscriptionId are correctly populated
Column errorsRegenerate the schema using the Better Auth CLI and apply the required database migrations

Helpful prompts

Use these prompts with an AI assistant (such as Claude) to accelerate common tasks during this integration.

Initial setup

Scaffold the full server configuration

I'm integrating Chargebee with Better Auth. Generate a complete auth.ts file that:
- Initializes the Chargebee SDK using CHARGEBEE_API_KEY and CHARGEBEE_SITE env vars
- Adds the chargebee plugin with createCustomerOnSignUp enabled
- Configures webhookUsername and webhookPassword from env vars
- Defines two plans: "starter" (itemPriceId: "starter-USD-Monthly", 5 projects, 10 GB storage)
  and "pro" (itemPriceId: "pro-USD-Monthly", 20 projects, 50 GB storage, 14-day free trial)
My database adapter is [Prisma / Drizzle / Kysely — fill in yours].

Generate the client plugin setup

Generate auth-client.ts for a Better Auth + Chargebee integration.
Include the chargebeeClient plugin with subscription enabled.
The base URL for the auth server is /api/auth.

Database migration

Understand what the migration will change

I'm about to run `npx auth migrate` after adding the Better Auth Chargebee plugin.
What tables and columns will be added or modified in my schema?
My adapter is [Prisma / Drizzle / Kysely — fill in yours].
List every change so I can review it before applying.

Fix column name errors after migration

After running `npx auth migrate` for the Better Auth Chargebee plugin I'm seeing this error:
[paste error here]
My adapter is [Prisma / Drizzle / Kysely] and my database is [Postgres / MySQL / SQLite].
What went wrong and how do I fix it?

Webhook setup

Generate a webhook verification utility

Write a TypeScript utility that verifies incoming Chargebee webhook requests using
HTTP Basic Auth. Read the expected username and password from
CHARGEBEE_WEBHOOK_USERNAME and CHARGEBEE_WEBHOOK_PASSWORD env vars.
Return 401 if credentials are missing or wrong, and export a reusable middleware
function compatible with [Next.js / Express / Hono — fill in yours].

Handle a specific webhook event

Write a TypeScript handler for the Chargebee `subscription_cancelled` webhook event
inside a Better Auth project. When the event fires:
1. Find the local user by chargebeeCustomerId.
2. Update their subscription status to "cancelled" in the database.
3. Send them a cancellation confirmation email using [Resend / Nodemailer — fill in yours].
Show the full handler including type definitions for the Chargebee event payload.

Test webhooks locally with ngrok

Walk me through testing Chargebee webhooks locally with ngrok and Better Auth.
My local dev server runs on port 3000 and my auth base path is /api/auth.
Include the exact ngrok command, the webhook URL to enter in the Chargebee dashboard,
and how to set matching Basic Auth credentials in my .env file.

Subscription flows

Gate a feature behind an active subscription

Using Better Auth with the Chargebee plugin, write a TypeScript function
`requireActivePlan(userId: string, planName: string): Promise<boolean>`
that returns true only if the user has an active or trialing subscription
matching the given plan name. Show how to call it in a Next.js API route
to protect a feature.

Build a pricing page that triggers checkout

Generate a React pricing page component that:
- Displays "Starter" ($9/mo) and "Pro" ($29/mo) cards
- Calls authClient.subscription.create() with the correct itemPriceId on click
- Uses successUrl: "/dashboard" and cancelUrl: "/pricing"
- Shows a loading state while the checkout redirects
- Handles the ALREADY_SUBSCRIBED error by redirecting to the billing portal instead

Load plans dynamically from the database

Refactor the static `plans` array in my Better Auth Chargebee plugin config to load
plans dynamically from a `plans` table. The table has columns:
name, chargebee_item_price_id, limits (JSON), trial_days (nullable).
Use [Prisma / Drizzle / Kysely — fill in yours]. Map the rows to the
shape expected by the plugin and handle the case where the query fails.

Organization billing

Set up org-scoped subscriptions with access control

I have the Better Auth organization plugin installed alongside the Chargebee plugin.
Show me how to:
1. Enable organization billing in the Chargebee plugin config.
2. Implement authorizeReference on the server so only org owners and admins
   can create or update a subscription for that organization.
3. Call authClient.subscription.create() from the client with customerType "organization"
   and the correct referenceId.

Debugging

Diagnose webhooks not being received

My Better Auth Chargebee webhooks are not reaching the server. Help me debug:
- Chargebee dashboard shows delivery attempts but the server logs show nothing.
- My auth base path is /api/auth.
- I'm running on [Vercel / Railway / a VPS — fill in yours].
Walk me through every layer to check: DNS, routing, middleware, and the plugin config.

Diagnose subscription status not updating

A user's subscription status in my database is not updating after they complete
checkout or cancel. My setup uses Better Auth with the Chargebee plugin.
What are the most likely causes and how do I verify each one step by step?
Include how to check whether chargebeeCustomerId and chargebeeSubscriptionId
are populated correctly.

Resources

Was this tutorial helpful ?
Need more help?

We're always happy to help you with any questions you might have! Click here to reach out to us.