Chargebee

用于 Better Auth 的 Chargebee 插件,用于管理订阅和支付。

Chargebee 插件将 Chargebee 的 订阅管理和计费功能与 Better Auth 集成。由于支付和认证通常紧密耦合,此插件简化了 Chargebee 集成到应用程序的流程,处理客户创建、订阅管理和 Webhook 处理。

此插件由 Chargebee 团队维护。关于错误、问题或功能请求, 请访问 Chargebee GitHub 仓库。

在 Chargebee Discord 上获取支持

有问题吗?我们的团队随时在 Discord 上为您提供帮助。

功能

  • 用户注册时自动创建 Chargebee 客户
  • 管理订阅计划和定价(基于项目:计划、附加组件、费用)
  • 处理订阅生命周期事件(创建、更新、取消)
  • 使用 Basic Auth 验证安全处理 Chargebee Webhook
  • 将订阅数据暴露给应用程序
  • 支持试用期和多项目订阅
  • 自动试用滥用预防——每个账户在所有计划中只能获得一次试用
  • 灵活的引用系统,用于将订阅与用户或组织关联
  • 团队订阅支持及席位管理
  • 通过 Chargebee 托管页面进行托管结账和门户
  • 自助计费门户,用于管理支付方式、发票和订阅

安装

安装插件

首先,安装插件:

npm install @chargebee/better-auth

如果您使用单独的客户端和服务器设置,请确保在项目的两个部分中都安装该插件。

安装 Chargebee SDK

接下来,在服务器上安装 Chargebee SDK:

npm install chargebee

将插件添加到认证配置

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({
    // ... 您的现有配置
    plugins: [
        chargebee({
            chargebeeClient,
            createCustomerOnSignUp: true,
            webhookUsername: process.env.CHARGEBEE_WEBHOOK_USERNAME,
            webhookPassword: process.env.CHARGEBEE_WEBHOOK_PASSWORD,
        })
    ]
})

添加客户端插件

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

export const authClient = createAuthClient({
    // ... 您的现有配置
    plugins: [
        chargebeeClient({
            subscription: true // 如果要启用订阅管理
        })
    ]
})

迁移数据库

运行迁移或生成架构以向数据库添加必要的表。

npx auth migrate

请参阅 架构 部分以手动添加表。

设置 Chargebee Webhook

在您的 Chargebee 仪表板中创建一个指向以下地址的 Webhook 端点:

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

/api/auth 是认证服务器的默认路径。

确保至少选择以下事件:

  • subscription_created
  • subscription_activated
  • subscription_changed
  • subscription_renewed
  • subscription_started
  • subscription_cancelled
  • subscription_cancellation_scheduled
  • customer_deleted

如果设置了 webhookUsername 和 webhookPassword,请在 Chargebee Webhook 设置中配置相同的 Basic Authentication 凭据。

用法

客户管理

您可以将此插件仅用于客户管理,而无需启用订阅。这在您只想将 Chargebee 客户与用户关联时非常有用。

当设置 createCustomerOnSignUp: true 时,注册时会自动创建一个 Chargebee 客户,并将其关联到数据库中的用户。 您可以自定义客户创建流程:

auth.ts
chargebee({
    // ... 其他选项
    createCustomerOnSignUp: true,
    onCustomerCreate: async ({ chargebeeCustomer, user }) => {
        // 对新创建的客户执行一些操作
        console.log(`Customer ${chargebeeCustomer.id} created for user ${user.id}`);
    },
})

传递额外的客户参数

Better Auth 将名称存储在单个 user.name 字段中。如果您希望在创建时传递 first_name、last_name 或任何其他 Chargebee 客户字段,请使用 getCustomerCreateParams:

auth.ts
chargebee({
    // ... 其他选项
    createCustomerOnSignUp: true,
    getCustomerCreateParams: (user) => {
        const [firstName, ...rest] = (user.name ?? "").split(" ");
        return {
            first_name: firstName || undefined,
            last_name: rest.join(" ") || undefined,
            // phone: user.phoneNumber,
            // 任何其他 Chargebee Customer.CreateInputParam 字段
        };
    },
})

此回调接收 user 对象和一个可选的 ctx(请求上下文——仅在按需创建客户时可用,而不是在注册期间)。

getCustomerCreateParams 适用于 createCustomerOnSignUp 和按需客户创建(例如,当用户首次订阅且没有先前注册时)。对于组织,请使用 organization.getCustomerCreateParams。

订阅管理

定义计划

Chargebee 使用基于项目的计费模型。您可以静态定义订阅计划,也可以从数据库中动态定义:

静态计划:

auth.ts
subscription: {
    enabled: true,
    plans: [
        {
            name: "starter", // 存储到数据库时计划名称会自动转为小写
            itemPriceId: "starter-USD-Monthly", // 来自 Chargebee 的 item price ID
            type: "plan",
            limits: {
                projects: 5,
                storage: 10
            }
        },
        {
            name: "pro",
            itemPriceId: "pro-USD-Monthly",
            type: "plan",
            limits: {
                projects: 20,
                storage: 50
            },
            freeTrial: {
                days: 14,
            }
        }
    ]
}

从数据库动态获取计划(推荐):

推荐从您自己的数据库中获取计划。这样您可以完全控制计划数据,使用自定义元数据(限制、功能、展示信息)丰富计划,并避免在认证配置中硬编码 Chargebee 配置:

auth.ts
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)
        }));
    }
}

更多内容请参见 计划配置。

创建订阅

要创建新订阅,请使用 subscription.create 方法:

POST/subscription/create
const { data, error } = await authClient.subscription.create({    itemPriceId: "pro-USD-Monthly", // required, 来自 Chargebee 的 item price ID(s)。单个字符串或数组用于多项目订阅。    referenceId: "123", // 订阅的引用 ID。默认基于 customerType。    metadata, // 要与订阅一起存储的附加元数据。    customerType, // 计费客户的类型。(默认:"user")    seats: 1, // 席位数量(如果适用)。    successUrl, // required, 用户完成付款或设置后重定向的 URL。    cancelUrl, // required, 如果设置,用户取消后将跳转至此。    disableRedirect: false, // required, 禁用订阅成功后的重定向。    trialEnd, // 试用结束时间(Unix 时间戳)。});
Parameters
itemPriceIdstring | string[]required

来自 Chargebee 的 item price ID(s)。单个字符串或数组用于多项目订阅。

referenceIdstring

订阅的引用 ID。默认基于 customerType。

metadataRecord<string, any>

要与订阅一起存储的附加元数据。

customerType"user" | "organization"

计费客户的类型。(默认:"user")

seatsnumber

席位数量(如果适用)。

successUrlstringrequired

用户完成付款或设置后重定向的 URL。

cancelUrlstringrequired

如果设置,用户取消后将跳转至此。

disableRedirectbooleanrequired

禁用订阅成功后的重定向。

trialEndnumber

试用结束时间(Unix 时间戳)。

简单示例:

这将创建一个 Chargebee Hosted Page,并将用户重定向到 Chargebee 结账页面。

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

结账重定向的工作原理

插件不会直接重定向到您的 successUrl。相反,它将 Chargebee 的 redirect_url 设置为一个内部注册的端点:

GET {baseURL}/subscription/success?callbackURL=<your-successUrl>&subscriptionId=<id>

Chargebee 在成功结账后访问该端点,然后插件会立即将用户转发到您原始的 successUrl。这为插件提供了一个钩子点,位于 Chargebee 的托管式页面重定向和您的应用程序之间,这样可以在不更改调用站点代码的情况下插入未来的中间件(例如会话刷新、订阅同步)。

切换计划

要将现有订阅切换到不同计划,请使用 subscription.update 方法。这确保用户只为新计划付费,而不是同时为两个计划付费:

POST/subscription/update
const { data, error } = await authClient.subscription.update({    itemPriceId: "pro-USD-Monthly", // required, 来自 Chargebee 的 item price ID(s)。单个字符串或数组用于多项目订阅。    referenceId: "123", // 订阅的引用 ID。默认基于 customerType。    subscriptionId: "sub_123", // 要更新的订阅 ID。    metadata, // 要与订阅一起存储的附加元数据。    customerType, // 计费客户的类型。(默认:"user")    seats: 1, // 要更新到的席位数量(如果适用)。    successUrl, // required, 用户完成付款或设置后重定向的 URL。    cancelUrl, // required, 如果设置,用户取消后将跳转至此。    returnUrl, // 返回的 URL。    disableRedirect: false, // required, 禁用重定向。});
Parameters
itemPriceIdstring | string[]required

来自 Chargebee 的 item price ID(s)。单个字符串或数组用于多项目订阅。

referenceIdstring

订阅的引用 ID。默认基于 customerType。

subscriptionIdstring

要更新的订阅 ID。

metadataRecord<string, any>

要与订阅一起存储的附加元数据。

customerType"user" | "organization"

计费客户的类型。(默认:"user")

seatsnumber

要更新到的席位数量(如果适用)。

successUrlstringrequired

用户完成付款或设置后重定向的 URL。

cancelUrlstringrequired

如果设置,用户取消后将跳转至此。

returnUrlstring

返回的 URL。

disableRedirectbooleanrequired

禁用重定向。

client.ts
await authClient.subscription.update({
    itemPriceId: "enterprise-USD-Monthly", // 新的 item price id
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
});

该插件仅支持每个引用 ID(用户或组织)同时只有一个活跃或试用中的订阅。使用 subscription.update 当用户已有活跃订阅并希望切换计划时。当用户没有活跃订阅时,请使用 subscription.create。

如果用户已有活跃订阅,则必须使用 subscription.update 并提供 subscriptionId 参数。否则,尝试使用 subscription.create 创建新订阅将因 ALREADY_SUBSCRIBED 错误而失败。

列出活跃订阅

要获取当前用户或组织的活跃订阅,请使用 subscription.list 方法:

GET/subscription/list
const { data, error } = await authClient.subscription.list({    query: {        referenceId, // 订阅的引用 ID。默认基于 customerType。        customerType, // 计费客户的类型。(默认:"user")    },});
Parameters
referenceIdstring

订阅的引用 ID。默认基于 customerType。

customerType"user" | "organization"

计费客户的类型。(默认:"user")

client.ts
const { data } = await authClient.subscription.list();
// data → 经过计划限制和 itemPriceId 增强的活跃/试用中订阅数组

// 对于组织:
const { data: orgSubscriptions } = await authClient.subscription.list({
    query: {
        referenceId: "org_123",
        customerType: "organization"
    }
});

取消订阅

要取消订阅,请使用 subscription.cancel 方法。这会将用户重定向到 Chargebee Portal,用户可以在那里取消订阅。当订阅在当前计费周期结束时取消时,Chargebee 会将其标记为 non_renewing。只有在周期结束时,订阅状态才会变为 cancelled。

POST/subscription/cancel
const { data, error } = await authClient.subscription.cancel({    referenceId: 'org_123', // 要取消的订阅的引用 ID。默认基于 customerType。    customerType, // 计费客户的类型。(默认:"user")    subscriptionId: 'sub_123', // 要取消的订阅的 ID。    returnUrl: '/account', // required, 用户点击返回您的网站链接时要跳转的 URL。});
Parameters
referenceIdstring

要取消的订阅的引用 ID。默认基于 customerType。

customerType"user" | "organization"

计费客户的类型。(默认:"user")

subscriptionIdstring

要取消的订阅的 ID。

returnUrlstringrequired

用户点击返回您的网站链接时要跳转的 URL。

理解取消状态

Chargebee 支持不同的取消行为:

字段描述
canceledAt订阅被取消的时间。
status当订阅结束时状态变为 "cancelled"。

计费门户会话

为了获得完整的自助计费体验,您可以打开 Chargebee 客户门户,让用户管理其计费的所有方面:

POST/subscription/portal
const { data, error } = await authClient.subscription.portal({    referenceId: 'org_123', // 订阅的引用 ID。默认基于 customerType。    customerType, // 计费客户的类型。(默认:"user")    returnUrl: '/account', // required, 用户完成门户会话后重定向的 URL。    disableRedirect: false, // 禁用重定向。});
Parameters
referenceIdstring

订阅的引用 ID。默认基于 customerType。

customerType"user" | "organization"

计费客户的类型。(默认:"user")

returnUrlstringrequired

用户完成门户会话后重定向的 URL。

disableRedirectboolean

禁用重定向。

示例:

client.ts
await authClient.subscription.portal({
    returnUrl: "/account/billing",
    fetchOptions: {
        onSuccess: (ctx) => {
            // 重定向到 Chargebee 门户
            window.location.href = ctx.data.url;
        }
    }
});

用于组织计费:

client.ts
await authClient.subscription.portal({
    referenceId: "org_123456",
    customerType: "organization",
    returnUrl: "/org/billing"
});

门户允许用户:

  • 更新支付方式(信用卡、银行账户)
  • 查看和下载发票
  • 管理订阅(升级、降级、取消)
  • 更新账单地址和联系人信息
  • 查看订阅历史记录
  • 应用促销优惠码

门户会话提供完整的自助服务体验,并推荐在您希望让用户完全控制其计费时,优于单独的操作(如取消)。

引用系统

默认情况下,订阅会关联到用户 ID。不过,您可以使用自定义引用 ID 将订阅关联到其他实体,例如组织:

client.ts
// 为组织创建订阅
await authClient.subscription.create({
    itemPriceId: "team-USD-Monthly",
    referenceId: "org_123456",
    customerType: "organization",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
    seats: 10 // 团队计划的座位数
});

// 列出组织的订阅
const { data: subscriptions } = await authClient.subscription.list({
    query: {
        referenceId: "org_123456",
        customerType: "organization"
    }
});

带席位数的团队订阅

对于团队或组织计划,您可以指定席位数量:

await authClient.subscription.create({
    itemPriceId: "team-USD-Monthly",
    referenceId: "org_123456",
    customerType: "organization",
    seats: 10, // 10 名团队成员
    successUrl: "/org/billing/success",
    cancelUrl: "/org/billing"
});

seats 参数将作为订阅项的数量传递给 Chargebee。您可以在应用逻辑中使用该值来限制团队或组织中的成员数量。

要授权引用 ID,请实现 authorizeReference 函数:

auth.ts
subscription: {
    // ... 其他选项
    authorizeReference: async ({ user, session, referenceId, action }) => {
        // 检查用户是否有权限管理此引用的订阅
        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;
    }
}

Webhook 处理

此插件会自动处理来自 Chargebee 的常见 Webhook 事件:

  • subscription_created – 在 Chargebee 中创建订阅时创建订阅。
  • subscription_activated – 当订阅在 Chargebee 中变为活跃时更新订阅。
  • subscription_changed – 当 Chargebee 中发生更改时更新订阅。
  • subscription_renewed – 在续订时更新订阅。
  • subscription_started – 当试用结束并且订阅开始时更新订阅。
  • subscription_cancelled – 将订阅标记为已取消。
  • subscription_cancellation_scheduled – 使用计划取消的详细信息更新订阅。
  • customer_deleted – 移除客户及其关联的任何订阅。

您也可以使用 webhookHandler 处理自定义事件,这为您提供了对类型化处理程序的直接访问:

auth.ts
import { WebhookEventType, WebhookHandler } from "chargebee"

chargebee({
    chargebeeClient,
    createCustomerOnSignUp: true,
    webhookHandler: (handler: WebhookHandler) => {
        handler.on(WebhookEventType.PaymentFailed, async ({ event }) => {
            // 处理支付失败
        });
        handler.on(WebhookEventType.InvoiceGenerated, async ({ event }) => {
            // 处理已生成的发票
        });
    }
})

订阅生命周期钩子

您可以在各种订阅生命周期事件中挂钩:

auth.ts
subscription: {
    // ... 其他选项
    onSubscriptionComplete: async ({ subscription, chargebeeSubscription, plan }) => {
        // 当通过托管页面成功创建订阅时调用
        await sendWelcomeEmail(subscription.referenceId, plan.name);
    },
    onSubscriptionCreated: async ({ subscription, chargebeeSubscription, plan }) => {
        // 当订阅被创建时调用
        await sendSubscriptionCreatedEmail(subscription.referenceId, plan.name);
    },
    onSubscriptionUpdate: async ({ subscription }) => {
        // 当订阅被更新时调用
        console.log(`Subscription ${subscription.id} updated`);
    },
    onSubscriptionDeleted: async ({ subscription, chargebeeSubscription }) => {
        // 当订阅被删除时调用
        await sendCancellationEmail(subscription.referenceId);
    },
    onTrialStart: async ({ subscription }) => {
        // 当试用开始时调用
        await sendTrialStartEmail(subscription.referenceId);
    },
    onTrialEnd: async ({ subscription }) => {
        // 当试用结束时调用
        await sendTrialEndEmail(subscription.referenceId);
    }
}

试用期

您可以为计划配置试用期:

auth.ts
{
    name: "pro",
    itemPriceId: "pro-USD-Monthly",
    type: "plan",
    freeTrial: {
        days: 14,  // 自动应用 14 天试用期
    },
    limits: {
        projects: 100,
        storage: 500,
    }
}

当用户订阅此计划时,试用期会自动应用——无需手动传递 trialEnd:

// 将根据计划配置自动计算并应用试用期
await authClient.subscription.create({
    itemPriceId: "pro-USD-Monthly",  // 带 14 天试用期的计划
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
});
// ✅ 用户会自动获得 14 天试用期!

插件会将试用结束日期计算为:当前日期 + 试用天数。

防止重复试用

为了防止用户获得多次试用,请启用 preventDuplicateTrials:

subscription: {
    enabled: true,
    plans,
    preventDuplicateTrials: true,  // 用户只能获得一次试用
}

覆盖试用结束日期(可选)

要设置自定义试用结束日期,请传入 trialEnd(Unix 时间戳):

await authClient.subscription.create({
    itemPriceId: "pro-USD-Monthly",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
    trialEnd: 1735689600,  // 自定义试用结束:2025 年 1 月 1 日
});

试用期仅适用于新订阅。对现有订阅的更新不能包含试用期(Chargebee 限制)。

架构

Chargebee 插件会向你的数据库添加以下表:

用户

表名:user

Table
Field
Type
Attributes
Description
chargebeeCustomerId ?
string
-
The Chargebee customer ID

组织

表名:organization (仅当 organization.enabled 为 true 时)

Table
Field
Type
Attributes
Description
chargebeeCustomerId ?
string
-
The Chargebee customer ID for the organization

订阅

表名:subscription

Table
Field
Type
Attributes
Description
id
string
PK
Unique identifier for each subscription
referenceId
string
-
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.
chargebeeCustomerId ?
string
-
The Chargebee customer ID
chargebeeSubscriptionId ?
string
-
The Chargebee subscription ID
status
string
-
The status of the subscription (future, in_trial, active, non_renewing, paused, cancelled, transferred)
periodStart ?
Date
-
Start date of the current billing period
periodEnd ?
Date
-
End date of the current billing period
trialStart ?
Date
-
Start date of the trial period
trialEnd ?
Date
-
End date of the trial period
canceledAt ?
Date
-
If the subscription has been canceled, this is the time when it was canceled
seats ?
number
-
Number of seats for team plans
metadata ?
string
-
JSON string of additional metadata

订阅项

表名:subscriptionItem

Table
Field
Type
Attributes
Description
id
string
PK
Unique identifier
subscriptionId
string
-
Foreign key to subscription
itemPriceId
string
-
Chargebee item price ID
itemType
string
-
Type: plan, addon, or charge
quantity
number
-
Quantity of this item
unitPrice ?
number
-
Unit price
amount ?
number
-
Total amount for this item

自定义架构

如果支持,你可以向 Chargebee 插件传入 schema 选项来更改架构表名或字段:

auth.ts
chargebee({
    // ... 其他选项
    schema: {
        subscription: {
            modelName: "chargebeeSubscriptions", // 将 subscription 表映射到 chargebeeSubscriptions
            fields: {
                referenceId: "userId" // 将 referenceId 字段映射到 userId
            }
        }
    }
})

选项

选项类型描述
chargebeeClientChargebeeChargebee 客户端实例。必需。
webhookUsernamestringWebhook 端点的基本认证用户名。建议在生产环境中使用。
webhookPasswordstringWebhook 端点的基本认证密码。建议在生产环境中使用。
createCustomerOnSignUpboolean用户注册时是否自动创建 Chargebee 客户。默认值:false。
getCustomerCreateParamsfunction返回传递给 cb.customer.create 的额外参数(用于用户客户),例如 first_name、last_name。接收 user 和可选的 ctx。
onCustomerCreatefunction客户创建后的回调函数。接收 { chargebeeCustomer, user }。
webhookHandlerfunction接收 webhook 处理程序实例的回调函数。调用 handler.on(EventType, fn) 来注册类型化的事件监听器。
subscriptionobject订阅配置。参见下面的 订阅选项。
organizationobject启用组织客户支持。参见下面的 组织选项。

订阅选项

选项类型描述
enabledboolean是否启用订阅功能。必需。
plansChargebeePlan[] 或 function订阅计划数组或返回计划的异步函数。如果启用,则必需。
requireEmailVerificationboolean是否要求在允许创建订阅之前进行电子邮件验证。默认值:false。
preventDuplicateTrialsboolean防止用户获得多个试用。默认值:false。
authorizeReferencefunction授权参考 ID。接收 { user, session, referenceId, action } 和上下文。
getHostedPageParamsfunction自定义 Chargebee Hosted Page 参数。接收 { user, session, plan, subscription }、请求和上下文。
onSubscriptionCompletefunction通过托管页面创建订阅后调用。接收 { subscription, chargebeeSubscription, plan }。
onSubscriptionCreatedfunction订阅创建后调用。接收 { subscription, chargebeeSubscription, plan }。
onSubscriptionUpdatefunction订阅更新时调用。接收 { subscription }。
onSubscriptionDeletedfunction订阅删除时调用。接收 { subscription, chargebeeSubscription }。
onTrialStartfunction试用开始时调用。接收 { subscription }。
onTrialEndfunction试用结束时调用。接收 { subscription }。

计划配置

选项类型描述
namestring计划名称。必需。
itemPriceIdstringChargebee 项目价格 ID。必需。
itemIdstringChargebee 项目 ID。可选。
itemFamilyIdstringChargebee 项目系列 ID。可选。
typestring类型:"plan"、"addon" 或 "charge"。必需。
limitsobject计划限制(例如 { projects: 10, storage: 5 })。
freeTrialobject试用配置。参见下面的 免费试用配置。
trialPeriodnumber试用期长度。可选。
trialPeriodUnitstring"day" 或 "month"。可选。
billingCyclesnumber计费周期数。可选。

免费试用配置

选项类型描述
daysnumber试用天数。必需。

组织选项

选项类型描述
enabledboolean启用组织客户支持。必需。
getCustomerCreateParamsfunction为组织自定义 Chargebee 客户创建参数。接收 organization 和上下文。
onCustomerCreatefunction组织客户创建后的回调函数。接收 { chargebeeCustomer, organization } 和上下文。

高级用法

与组织一起使用

Chargebee 插件与 organization 插件 集成,可将组织作为 Chargebee 客户使用。订阅的计费主体不再是单个用户,而是组织。对于 B2B 服务,这很有用,因为计费绑定到组织而不是个人用户。

启用组织客户时:

  • 组织首次订阅时,会自动创建 Chargebee 客户
  • 组织名称更改会同步到 Chargebee 客户
  • 拥有活动订阅的组织无法被删除

启用组织客户

要启用组织客户,请将 organization.enabled 设置为 true,并确保已安装 organization 插件:

auth.ts
plugins: [
    organization(),
    chargebee({
        // ... 其他选项
        subscription: {
            enabled: true,
            plans: [...],
        },
        organization: { 
            enabled: true
        } 
    })
]

当 organization.enabled: true 时,插件会自动从 user 表中排除 chargebeeCustomerId 并禁用用户级计费钩子。使用仅限组织范围的计费时,无需将该列添加到数据库架构中。

创建组织订阅

即使启用了组织客户,用户订阅仍然可用,并且是默认选项。若要使用组织作为计费主体,请传入 customerType: "organization":

client.ts
await authClient.subscription.create({
    itemPriceId: "team-USD-Monthly",
    referenceId: activeOrg.id,
    customerType: "organization", 
    seats: 10,
    successUrl: "/org/billing/success",
    cancelUrl: "/org/billing"
});

授权

请务必实现 authorizeReference 函数,以验证用户是否有权限管理该组织的订阅:

auth.ts
subscription: {
    // ... 其他订阅选项
    authorizeReference: async ({ user, referenceId, action }) => {
        const member = await db.members.findFirst({
            where: {
                userId: user.id,
                organizationId: referenceId
            }
        });

        return member?.role === "owner" || member?.role === "admin";
    }
}

组织计费邮箱

与用户不同,组织的计费邮箱不会自动同步,因为组织本身没有唯一的邮箱。组织通常会使用一个与用户账户分开的专用计费邮箱。 要在结账后更改计费邮箱,请通过 Chargebee Dashboard 更新,或使用 chargebeeClient 实现自定义逻辑:

await chargebeeClient.customer.update(organization.chargebeeCustomerId, {
    email: "[email protected]"
});

自定义 Hosted Page 参数

你可以为 Chargebee Hosted Page 自定义附加参数:

auth.ts
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"
    };
}

试用期管理

Chargebee 插件会自动防止用户获得多个免费试用期。一旦用户使用过一次试用期(无论哪个计划),他们就不再有资格在任何计划上获得额外试用。

工作原理:

  • 系统会跟踪每个用户在所有计划中的试用使用情况
  • 当用户订阅带有试用的计划时,系统会检查其订阅历史记录
  • 如果用户曾经有过试用(通过 trialStart/trialEnd 字段或 in_trial 状态标识),则不会提供新的试用
  • 这可以防止用户取消订阅后重新注册以获得多个免费试用

示例场景:

  1. 用户订阅带有 7 天试用期的 "Starter" 计划
  2. 用户在试用期结束后取消订阅
  3. 用户尝试订阅 "Premium" 计划——不会提供试用
  4. 用户将立即被收取 Premium 计划费用

启用 preventDuplicateTrials 后,这种行为会自动生效,无需额外配置。试用资格会在创建订阅时确定,且不能通过配置覆盖。

故障排查

列/字段命名错误

如果你看到类似 no such column: "chargebee_customer_id" 或 no such column: "chargebeeCustomerId" 的错误:

原因: 数据库列名与适配器的 schema 定义不匹配。

解决方案:

  1. 运行 npx auth generate,使用 Chargebee 插件字段重新生成 schema。
  2. 运行 npx auth migrate 将迁移应用到数据库,或使用 ORM 的迁移工具。
  3. 如果是从另一个适配器手动迁移,请确保列名符合数据库适配器的命名约定。
  4. 查阅数据库文档,了解与你的适配器(Prisma、Drizzle、Kysely 等)对应的字段名映射。

Webhook 问题

如果 webhook 没有被正确处理:

  1. 检查 webhook URL 是否已在 Chargebee dashboard 中正确配置
  2. 验证 Basic Auth 凭据(webhookUsername 和 webhookPassword)是否正确
  3. 确保已在 Chargebee dashboard 中选择所有必要事件
  4. 检查服务器日志中 webhook 处理期间是否有任何错误

订阅状态问题

如果订阅状态没有正确更新:

  1. 确保 webhook 事件已被接收并处理
  2. 检查 chargebeeCustomerId 和 chargebeeSubscriptionId 字段是否已正确填充
  3. 验证应用和 Chargebee 之间的 reference ID 是否匹配

在本地测试 Webhook

对于本地开发,你可以使用隧道(例如 ngrok)将 webhook 转发到本地环境:

ngrok http 3000

然后将 Chargebee webhook 配置为指向:

https://your-ngrok-url/api/auth/chargebee/webhook

请确保在 Chargebee 和本地环境变量中使用相同的 Basic Auth 凭据。

资源