docs/content/docs/plugins/chargebee.mdx
The Chargebee plugin integrates Chargebee's subscription management and billing functionality with Better Auth. Since payment and authentication are often tightly coupled, this plugin simplifies the integration of Chargebee into your application, handling customer creation, subscription management, and webhook processing.
<Callout> This plugin is maintained by the Chargebee team. For bugs, issues or feature requests, please visit the [Chargebee GitHub repo](https://github.com/chargebee/js-framework-adapters). </Callout> <Card href="https://discord.com/invite/gpsNqnhDm2" title="Get support on Chargebee Discord"> Have questions? Our team is available on Discord to assist you anytime. </Card>First, install the plugin:
```package-install
@chargebee/better-auth
```
<Callout>
If you're using a separate client and server setup, make sure to install the plugin in both parts of your project.
</Callout>
Next, install the Chargebee SDK on your server:
```package-install
chargebee
```
```ts title="auth.ts"
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,
})
]
})
```
```ts title="auth-client.ts"
import { createAuthClient } from "better-auth/client"
import { chargebeeClient } from "@chargebee/better-auth/client"
export const authClient = createAuthClient({
// ... your existing config
plugins: [
chargebeeClient({
subscription: true //if you want to enable subscription management
})
]
})
```
Run the migration or generate the schema to add the necessary tables to the database.
<Tabs items={["migrate", "generate"]}>
<Tab value="migrate">
```bash
npx auth migrate
```
</Tab>
<Tab value="generate">
```bash
npx auth generate
```
</Tab>
</Tabs>
See the [Schema](#schema) section to add the tables manually.
Create a webhook endpoint in your Chargebee dashboard pointing to:
```
https://your-domain.com/api/auth/chargebee/webhook
```
`/api/auth` is the default path for the auth server.
Make sure to select at least these events:
* `subscription_created`
* `subscription_activated`
* `subscription_changed`
* `subscription_renewed`
* `subscription_started`
* `subscription_cancelled`
* `subscription_cancellation_scheduled`
* `customer_deleted`
If you set `webhookUsername` and `webhookPassword`, configure the same Basic Authentication credentials in the Chargebee webhook settings.
You can use this plugin solely for customer management without enabling subscriptions. This is useful if you just want to link Chargebee customers to your users.
When you set createCustomerOnSignUp: true, a Chargebee customer is automatically created on signup and linked to the user in your database.
You can customize the customer creation process:
chargebee({
// ... other options
createCustomerOnSignUp: true,
onCustomerCreate: async ({ chargebeeCustomer, user }) => {
// Do something with the newly created customer
console.log(`Customer ${chargebeeCustomer.id} created for user ${user.id}`);
},
})
Better Auth stores names in a single user.name field. If you want to pass first_name, last_name, or any other Chargebee customer field at creation time, use getCustomerCreateParams:
chargebee({
// ... other options
createCustomerOnSignUp: true,
getCustomerCreateParams: (user) => {
const [firstName, ...rest] = (user.name ?? "").split(" ");
return {
first_name: firstName || undefined,
last_name: rest.join(" ") || undefined,
// phone: user.phoneNumber,
// any other Chargebee Customer.CreateInputParam fields
};
},
})
The callback receives the user object and an optional ctx (request context — only available when the customer is created on-demand at subscription time, not during sign-up).
Chargebee uses an item-based billing model. You can define your subscription plans either statically or dynamically from your database:
Static plans:
subscription: {
enabled: true,
plans: [
{
name: "starter", // the name of the plan, it'll be automatically lower cased when stored in the database
itemPriceId: "starter-USD-Monthly", // the item price ID from Chargebee
type: "plan",
limits: {
projects: 5,
storage: 10
}
},
{
name: "pro",
itemPriceId: "pro-USD-Monthly",
type: "plan",
limits: {
projects: 20,
storage: 50
},
freeTrial: {
days: 14,
}
}
]
}
Dynamic plans from database (Recommended):
Fetching plans from your own database is the recommended approach. It gives you full control over plan data, lets you enrich plans with custom metadata (limits, features, display info), and avoids hard-coding Chargebee configuration into your auth setup:
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)
}));
}
}
see plan configuration for more.
To create a new subscription, use the subscription.create method:
Simple Example:
This will create a Chargebee Hosted Page and redirect the user to the Chargebee checkout page.
await authClient.subscription.create({
itemPriceId: "pro-USD-Monthly",
successUrl: "/dashboard",
cancelUrl: "/pricing",
});
The plugin does not redirect straight to your successUrl. Instead it sets Chargebee's redirect_url to an internally registered endpoint:
GET {baseURL}/subscription/success?callbackURL=<your-successUrl>&subscriptionId=<id>
Chargebee lands on that endpoint after a successful checkout, and the plugin immediately forwards the user to your original successUrl. This gives the plugin a hook point between Chargebee's hosted-page redirect and your application so that future middleware (e.g. session refresh, subscription sync) can be inserted without changing your call-site code.
</Callout>
To switch an existing subscription to a different plan, use the subscription.update method. This ensures that the user only pays for the new plan, and not both:
await authClient.subscription.update({
itemPriceId: "enterprise-USD-Monthly", // new item price id
successUrl: "/dashboard",
cancelUrl: "/pricing",
});
If the user already has an active subscription, you must use subscription.update and provide the subscriptionId parameter when needed. Otherwise, attempting to create a new subscription via subscription.create will fail with an ALREADY_SUBSCRIBED error.
</Callout>
To retrieve the active subscriptions for the current user or organization, use the subscription.list method:
const { data } = await authClient.subscription.list();
// data → array of active/trialing subscriptions enriched with plan limits and itemPriceId
// For an organization:
const { data: orgSubscriptions } = await authClient.subscription.list({
query: {
referenceId: "org_123",
customerType: "organization"
}
});
To cancel a subscription, use the subscription.cancel method. This redirects the user to the Chargebee Portal where they can cancel their subscription. When a subscription is canceled at the end of the current billing period, Chargebee marks it as non_renewing. The subscription status changes to cancelled only when the period ends.
Chargebee supports different cancellation behaviors:
| Field | Description |
|---|---|
canceledAt | The time when the subscription was canceled. |
status | Changes to "cancelled" when the subscription has ended. |
For a complete self-service billing experience, you can open the Chargebee customer portal where users can manage all aspects of their billing:
<APIMethod path="/subscription/portal" method="POST" requireSession> ```ts type createPortalSession = { /** * Reference id of the customer. Defaults based on customerType. */ referenceId?: string = 'org_123' /** * The type of customer for billing. (Default: "user") */ customerType?: "user" | "organization" /** * URL to redirect customers to after they complete their portal session. */ returnUrl: string = '/account' /** * Disable redirect after opening portal. */ disableRedirect?: boolean = false } ``` </APIMethod>Example:
await authClient.subscription.portal({
returnUrl: "/account/billing",
fetchOptions: {
onSuccess: (ctx) => {
// Redirect to Chargebee portal
window.location.href = ctx.data.url;
}
}
});
For organization billing:
await authClient.subscription.portal({
referenceId: "org_123456",
customerType: "organization",
returnUrl: "/org/billing"
});
The portal allows users to:
By default, subscriptions are associated with the user ID. However, you can use a custom reference ID to associate subscriptions with other entities, such as organizations:
// Create a subscription for an organization
await authClient.subscription.create({
itemPriceId: "team-USD-Monthly",
referenceId: "org_123456",
customerType: "organization",
successUrl: "/dashboard",
cancelUrl: "/pricing",
seats: 10 // Number of seats for team plans
});
// List subscriptions for an organization
const { data: subscriptions } = await authClient.subscription.list({
query: {
referenceId: "org_123456",
customerType: "organization"
}
});
For team or organization plans, you can specify the number of seats:
await authClient.subscription.create({
itemPriceId: "team-USD-Monthly",
referenceId: "org_123456",
customerType: "organization",
seats: 10, // 10 team members
successUrl: "/org/billing/success",
cancelUrl: "/org/billing"
});
The seats parameter is passed to Chargebee as the quantity for the subscription item. You can use this value in your application logic to limit the number of members in a team or organization.
To authorize reference IDs, implement the authorizeReference function:
subscription: {
// ... other options
authorizeReference: async ({ user, session, referenceId, action }) => {
// Check if the user has permission to manage subscriptions for this reference
if (action === "create-subscription" || action === "update-subscription" || action === "cancel-subscription" || action === "billing-portal") {
const org = await db.member.findFirst({
where: {
organizationId: referenceId,
userId: user.id
}
});
return org?.role === "owner"
}
return true;
}
}
The plugin automatically processes common webhook events from Chargebee:
subscription_created – Creates a subscription when it is created in Chargebee.subscription_activated – Updates the subscription when it becomes active in Chargebee.subscription_changed – Updates the subscription when changes are made in Chargebee.subscription_renewed – Updates the subscription upon renewal.subscription_started – Updates the subscription when the trial ends and the subscription starts.subscription_cancelled – Marks the subscription as canceled.subscription_cancellation_scheduled – Updates the subscription with the scheduled cancellation details.customer_deleted – Removes the customer and any associated subscriptions.You can also handle custom events using webhookHandler, which gives you direct access to the typed handler instance:
import { WebhookEventType, WebhookHandler } from "chargebee"
chargebee({
chargebeeClient,
createCustomerOnSignUp: true,
webhookHandler: (handler: WebhookHandler) => {
handler.on(WebhookEventType.PaymentFailed, async ({ event }) => {
// Handle failed payment
});
handler.on(WebhookEventType.InvoiceGenerated, async ({ event }) => {
// Handle generated invoice
});
}
})
You can hook into various subscription lifecycle events:
subscription: {
// ... other options
onSubscriptionComplete: async ({ subscription, chargebeeSubscription, plan }) => {
// Called when a subscription is successfully created via hosted page
await sendWelcomeEmail(subscription.referenceId, plan.name);
},
onSubscriptionCreated: async ({ subscription, chargebeeSubscription, plan }) => {
// Called when a subscription is created
await sendSubscriptionCreatedEmail(subscription.referenceId, plan.name);
},
onSubscriptionUpdate: async ({ subscription }) => {
// Called when a subscription is updated
console.log(`Subscription ${subscription.id} updated`);
},
onSubscriptionDeleted: async ({ subscription, chargebeeSubscription }) => {
// Called when a subscription is deleted
await sendCancellationEmail(subscription.referenceId);
},
onTrialStart: async ({ subscription }) => {
// Called when a trial starts
await sendTrialStartEmail(subscription.referenceId);
},
onTrialEnd: async ({ subscription }) => {
// Called when a trial ends
await sendTrialEndEmail(subscription.referenceId);
}
}
You can configure trial periods for your plans:
{
name: "pro",
itemPriceId: "pro-USD-Monthly",
type: "plan",
freeTrial: {
days: 14, // 14-day trial automatically applied
},
limits: {
projects: 100,
storage: 500,
}
}
When a user subscribes to this plan, the trial is automatically applied - no need to pass trialEnd manually:
// Trial is automatically calculated and applied based on plan config
await authClient.subscription.create({
itemPriceId: "pro-USD-Monthly", // Plan with 14-day trial
successUrl: "/dashboard",
cancelUrl: "/pricing",
});
// ✅ User gets 14-day trial automatically!
The plugin calculates the trial end date as: current date + trial days.
To prevent users from getting multiple trials, enable preventDuplicateTrials:
subscription: {
enabled: true,
plans,
preventDuplicateTrials: true, // Users can only get one trial
}
To set a custom trial end date, pass trialEnd (Unix timestamp):
await authClient.subscription.create({
itemPriceId: "pro-USD-Monthly",
successUrl: "/dashboard",
cancelUrl: "/pricing",
trialEnd: 1735689600, // Custom trial end: Jan 1, 2025
});
The Chargebee plugin adds the following tables to your database:
Table Name: user
export const chargebeeUserTableFields = [ { name: "chargebeeCustomerId", type: "string", description: "The Chargebee customer ID", isOptional: true, }, ];
<DatabaseTable name="user" fields={chargebeeUserTableFields} />Table Name: organization <small className="text-xs">(only when organization.enabled is true)</small>
export const chargebeeOrganizationTableFields = [ { name: "chargebeeCustomerId", type: "string", description: "The Chargebee customer ID for the organization", isOptional: true, }, ];
<DatabaseTable name="organization" fields={chargebeeOrganizationTableFields} />Table Name: subscription
export const chargebeeSubscriptionTableFields = [ { name: "id", type: "string", description: "Unique identifier for each subscription", isPrimaryKey: true, }, { name: "referenceId", type: "string", description: "The ID this subscription is associated with (user ID by default). This should NOT be a unique field in your database, as it must allow users to resubscribe after a cancellation.", isUnique: false, }, { name: "chargebeeCustomerId", type: "string", description: "The Chargebee customer ID", isOptional: true, }, { name: "chargebeeSubscriptionId", type: "string", description: "The Chargebee subscription ID", isOptional: true, }, { name: "status", type: "string", description: "The status of the subscription (future, in_trial, active, non_renewing, paused, cancelled, transferred)", defaultValue: "future", }, { name: "periodStart", type: "Date", description: "Start date of the current billing period", isOptional: true, }, { name: "periodEnd", type: "Date", description: "End date of the current billing period", isOptional: true, }, { name: "trialStart", type: "Date", description: "Start date of the trial period", isOptional: true, }, { name: "trialEnd", type: "Date", description: "End date of the trial period", isOptional: true, }, { name: "canceledAt", type: "Date", description: "If the subscription has been canceled, this is the time when it was canceled", isOptional: true, }, { name: "seats", type: "number", description: "Number of seats for team plans", isOptional: true, }, { name: "metadata", type: "string", description: "JSON string of additional metadata", isOptional: true, }, ];
<DatabaseTable name="subscription" fields={chargebeeSubscriptionTableFields} />Table Name: subscriptionItem
export const chargebeeSubscriptionItemTableFields = [ { name: "id", type: "string", description: "Unique identifier", isPrimaryKey: true, }, { name: "subscriptionId", type: "string", description: "Foreign key to subscription", }, { name: "itemPriceId", type: "string", description: "Chargebee item price ID", }, { name: "itemType", type: "string", description: "Type: plan, addon, or charge", }, { name: "quantity", type: "number", description: "Quantity of this item", }, { name: "unitPrice", type: "number", description: "Unit price", isOptional: true, }, { name: "amount", type: "number", description: "Total amount for this item", isOptional: true, }, ];
<DatabaseTable name="subscriptionItem" fields={chargebeeSubscriptionItemTableFields} />To change the schema table names or fields, you can pass a schema option to the Chargebee plugin (if supported):
chargebee({
// ... other options
schema: {
subscription: {
modelName: "chargebeeSubscriptions", // map the subscription table to chargebeeSubscriptions
fields: {
referenceId: "userId" // map the referenceId field to userId
}
}
}
})
| Option | Type | Description |
|---|---|---|
chargebeeClient | Chargebee | The Chargebee client instance. Required. |
webhookUsername | string | Username for Basic Auth on the webhook endpoint. Recommended in production. |
webhookPassword | string | Password for Basic Auth on the webhook endpoint. Recommended in production. |
createCustomerOnSignUp | boolean | Whether to automatically create a Chargebee customer when a user signs up. Default: false. |
getCustomerCreateParams | function | Return additional params passed to cb.customer.create for user customers (e.g. first_name, last_name). Receives user and an optional ctx. |
onCustomerCreate | function | Callback called after a customer is created. Receives { chargebeeCustomer, user }. |
webhookHandler | function | Callback receiving the webhook handler instance. Call handler.on(EventType, fn) to register typed event listeners. |
subscription | object | Subscription configuration. See below. |
organization | object | Enable Organization Customer support. See below. |
| Option | Type | Description |
|---|---|---|
enabled | boolean | Whether to enable subscription functionality. Required. |
plans | ChargebeePlan[] or function | An array of subscription plans or an async function that returns plans. Required if enabled. |
requireEmailVerification | boolean | Whether to require email verification before allowing subscription creation. Default: false. |
preventDuplicateTrials | boolean | Prevent users from getting multiple trials. Default: false. |
authorizeReference | function | Authorize reference IDs. Receives { user, session, referenceId, action } and context. |
getHostedPageParams | function | Customize Chargebee Hosted Page parameters. Receives { user, session, plan, subscription }, request, and context. |
onSubscriptionComplete | function | Called when a subscription is created via hosted page. Receives { subscription, chargebeeSubscription, plan }. |
onSubscriptionCreated | function | Called when a subscription is created. Receives { subscription, chargebeeSubscription, plan }. |
onSubscriptionUpdate | function | Called when a subscription is updated. Receives { subscription }. |
onSubscriptionDeleted | function | Called when a subscription is deleted. Receives { subscription, chargebeeSubscription }. |
onTrialStart | function | Called when a trial starts. Receives { subscription }. |
onTrialEnd | function | Called when a trial ends. Receives { subscription }. |
| Option | Type | Description |
|---|---|---|
name | string | The name of the plan. Required. |
itemPriceId | string | The Chargebee item price ID. Required. |
itemId | string | The Chargebee item ID. Optional. |
itemFamilyId | string | The Chargebee item family ID. Optional. |
type | string | Type: "plan", "addon", or "charge". Required. |
limits | object | Limits for plan (e.g. { projects: 10, storage: 5 }). |
freeTrial | object | Trial configuration. See below. |
trialPeriod | number | Trial period length. Optional. |
trialPeriodUnit | string | "day" or "month". Optional. |
billingCycles | number | Number of billing cycles. Optional. |
| Option | Type | Description |
|---|---|---|
days | number | Number of trial days. Required. |
| Option | Type | Description |
|---|---|---|
enabled | boolean | Enable Organization Customer support. Required. |
getCustomerCreateParams | function | Customize Chargebee customer creation parameters for organizations. Receives organization and context. |
onCustomerCreate | function | Called after an organization customer is created. Receives { chargebeeCustomer, organization } and context. |
The Chargebee plugin integrates with the organization plugin to enable organizations as Chargebee Customers. Instead of individual users, organizations become the billing entity for subscriptions. This is useful for B2B services where billing is tied to the organization rather than individual user.
<Callout type="info"> **When Organization Customer is enabled:**To enable Organization Customer, set organization.enabled to true and ensure the organization plugin is installed:
plugins: [
organization(),
chargebee({
// ... other options
subscription: {
enabled: true,
plans: [...],
},
organization: { // [!code highlight]
enabled: true // [!code highlight]
} // [!code highlight]
})
]
Even with Organization Customer enabled, user subscriptions remain available and are the default. To use the organization as the billing entity, pass customerType: "organization":
await authClient.subscription.create({
itemPriceId: "team-USD-Monthly",
referenceId: activeOrg.id,
customerType: "organization", // [!code highlight]
seats: 10,
successUrl: "/org/billing/success",
cancelUrl: "/org/billing"
});
Make sure to implement the authorizeReference function to verify that the user has permission to manage subscriptions for the organization:
subscription: {
// ... other subscription options
authorizeReference: async ({ user, referenceId, action }) => {
const member = await db.members.findFirst({
where: {
userId: user.id,
organizationId: referenceId
}
});
return member?.role === "owner" || member?.role === "admin";
}
}
Unlike users, organization billing email is not automatically synced because organization itself doesn't have a unique email. Organizations often use a dedicated billing email separate from user accounts.
To change the billing email after checkout, update it through the Chargebee Dashboard or implement custom logic using chargebeeClient:
await chargebeeClient.customer.update(organization.chargebeeCustomerId, {
email: "[email protected]"
});
You can customize the Chargebee Hosted Page with additional parameters:
getHostedPageParams: async ({ user, session, plan, subscription }, request, ctx) => {
return {
embed: false,
layout: "in_app",
pass_thru_content: JSON.stringify({
userId: user.id,
planType: "business"
}),
redirect_url: "https://yourdomain.com/success",
cancel_url: "https://yourdomain.com/cancel"
};
}
The Chargebee plugin automatically prevents users from getting multiple free trials. Once a user has used a trial period (regardless of which plan), they will not be eligible for additional trials on any plan.
How it works:
trialStart/trialEnd fields or in_trial status), no new trial will be offeredExample scenario:
This behavior is automatic and requires no additional configuration when preventDuplicateTrials is enabled. The trial eligibility is determined at the time of subscription creation and cannot be overridden through configuration.
If you see errors like no such column: "chargebee_customer_id" or no such column: "chargebeeCustomerId":
Cause: Mismatch between your database column names and your adapter's schema definition.
Solution:
npx better-auth generate to regenerate your schema with the Chargebee plugin fieldsIf webhooks aren't being processed correctly:
webhookUsername and webhookPassword) are correctIf subscription statuses aren't updating correctly:
chargebeeCustomerId and chargebeeSubscriptionId fields are correctly populatedFor local development, you can use a tunnel (e.g. ngrok) to forward webhooks to your local environment:
ngrok http 3000
Then configure your Chargebee webhook to point to:
https://your-ngrok-url/api/auth/chargebee/webhook
Make sure to use the same Basic Auth credentials in Chargebee and in your local environment variables.