Stripe

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

Stripe 插件将 Stripe 的支付与订阅功能集成到 Better Auth 中。由于支付和认证通常紧密相关,此插件简化了在应用中集成 Stripe 的流程,负责客户创建、订阅管理和 webhook 处理。

功能

  • 创建用户注册时的 Stripe 客户
  • 管理订阅计划和定价
  • 处理订阅生命周期事件(创建、更新、取消)
  • 安全处理 Stripe webhook 并进行签名验证
  • 向应用暴露订阅数据
  • 支持试用期和订阅升级
  • 自动试用期滥用防护 - 每个账户在所有计划中只能获得一次试用
  • 灵活的引用系统,用于关联订阅与用户或组织
  • 支持带席位管理的团队订阅

安装

安装插件

首先,安装插件:

npm install @better-auth/stripe

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

安装 Stripe SDK

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

npm install stripe@^22.0.0

将插件添加到认证配置

auth.ts
import { betterAuth } from "better-auth"
import { stripe } from "@better-auth/stripe"
import Stripe from "stripe"

const stripeClient = new Stripe(process.env.STRIPE_SECRET_KEY!, {
    apiVersion: "2026-03-25.dahlia", // Stripe SDK v22.0.0 最新的 API 版本
})

export const auth = betterAuth({
    // ... 现有配置
    plugins: [
        stripe({
            stripeClient,
            stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
            createCustomerOnSignUp: true,
        })
    ]
})

从 Stripe v18 升级? 版本 19 使用异步 webhook 签名验证(constructEventAsync),由插件内部处理。您端无需进行任何代码更改!

添加客户端插件

auth-client.ts
import { createAuthClient } from "better-auth/client"
import { stripeClient } from "@better-auth/stripe/client"

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

迁移数据库

运行迁移或生成模式以将必要的表添加到数据库。

npx auth migrate
npx auth generate

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

设置 Stripe webhook

在 Stripe 仪表板中创建一个 webhook 端点,指向:

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

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

确保选择以下事件:

  • checkout.session.completed
  • customer.subscription.created
  • customer.subscription.updated
  • customer.subscription.deleted

保存 Stripe 提供的 webhook 签名密钥,并将其添加到环境变量中,名为 STRIPE_WEBHOOK_SECRET

使用

客户管理

您可以仅使用此插件进行客户管理而不启用订阅。适用于只想关联 Stripe 客户和用户的场景。

当设置 createCustomerOnSignUp: true 时,用户注册时会自动创建 Stripe 客户,并与数据库中的用户关联。您也可以自定义客户创建流程:

auth.ts
stripe({
    // ... 其他选项
    createCustomerOnSignUp: true,
    onCustomerCreate: async ({ stripeCustomer, user }, ctx) => {
        // 处理新创建的客户
        console.log(`客户 ${stripeCustomer.id} 已为用户 ${user.id} 创建`);
    },
    getCustomerCreateParams: async (user, ctx) => {
        // 定制 Stripe 客户创建参数
        return {
            metadata: {
                referralSource: user.metadata?.referralSource
            }
        };
    }
})

订阅管理

定义计划

您可以静态定义订阅计划,也可以动态获取:

auth.ts
// 静态定义
subscription: {
    enabled: true,
    plans: [
        {
            name: "basic", // 计划名称,存入数据库时自动转为小写
            priceId: "price_1234567890", // Stripe 价格 ID
            annualDiscountPriceId: "price_1234567890", // (可选)年付折扣价格 ID
            limits: {
                projects: 5,
                storage: 10
            }
        },
        {
            name: "pro",
            priceId: "price_0987654321",
            limits: {
                projects: 20,
                storage: 50
            },
            freeTrial: {
                days: 14,
            }
        }
    ]
}

// 动态定义(从数据库或接口获取)
subscription: {
    enabled: true,
    plans: async () => {
        const plans = await db.query("SELECT * FROM plans");
        return plans.map(plan => ({
            name: plan.name,
            priceId: plan.stripe_price_id,
            limits: JSON.parse(plan.limits)
        }));
    }
}

详情参见 计划配置

创建订阅

通过调用 subscription.upgrade 方法创建订阅:

POST/subscription/upgrade
const { data, error } = await authClient.subscription.upgrade({    plan: "pro", // required    annual: true,    referenceId: "123",    subscriptionId: "sub_123",    metadata,    customerType,    seats: 1,    locale,    successUrl, // required    cancelUrl, // required    returnUrl,    disableRedirect: false, // required    scheduleAtPeriodEnd: false,});
Parameters
planstringrequired

要升级到的计划名称。

annualboolean

是否升级为年付计划。

referenceIdstring

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

subscriptionIdstring

要升级的订阅 ID。

metadataRecord<string, any>

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

customerType"user" | "organization"

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

seatsnumber

升级到的席位数量(如果适用)。

localestring

Checkout 显示的语言环境 IETF 标签。 如果未提供或设置为 "auto",则使用浏览器语言环境。

successUrlstringrequired

Stripe 完成支付或设置后要跳转到的 URL。

cancelUrlstringrequired

如果设置,将显示返回按钮,用户取消支付时将跳转至此。

returnUrlstring

从计费门户返回时跳转的 URL(用于升级现有订阅)。

disableRedirectbooleanrequired

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

scheduleAtPeriodEndboolean

在当前计费周期结束时计划更改,而不是立即应用。

简单示例:

client.ts
await authClient.subscription.upgrade({
    plan: "pro",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
    annual: true, // 可选:升级为年付计划
    referenceId: "org_123", // 可选:默认为 customerType 的值
    seats: 5, // 可选:团队计划席位数量
    locale: "en" // 可选:用英文显示 Checkout
});

这将创建一个 Checkout 会话并将用户重定向到 Stripe Checkout 页面。

该插件每个引用 ID(用户或组织)一次仅支持一个活跃或试用中的订阅。同一引用 ID 下不允许同时存在多个订阅。

如果用户已有活跃订阅,则 必须 在升级时提供 subscriptionId 参数。否则,可能会在与现有订阅并行的位置创建新订阅,导致重复计费。

重要提示: successUrl 参数将在内部被修改,以处理结账完成与 webhook 处理之间的竞争条件。插件会创建一个跳转中转页面,在订阅状态正确更新后才跳转到您指定的成功页。

const { error } = await authClient.subscription.upgrade({
    plan: "pro",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
});
if(error) {
    alert(error.message);
}

切换计划

要切换订阅计划,可以调用 subscription.upgrade

要将订阅切换到不同的计划,请使用 subscription.upgrade 方法:

client.ts
await authClient.subscription.upgrade({
    plan: "pro",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
    subscriptionId: "sub_123", // 当前用户计划的 Stripe 订阅 ID
});

这确保用户只支付新计划费用,而不会同时支付两个计划。

确保用户只支付新计划,不会同时支付两个计划费用。

在周期结束时调度计划更改

默认情况下,计划更改会立即生效并自动按比例计费。如果希望用户继续使用当前计划直到结算周期结束,然后切换计划,可以这样做:

client.ts
await authClient.subscription.upgrade({
    plan: "pro",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
    returnUrl: "/billing",
    scheduleAtPeriodEnd: true, // 默认 false
});

此方案使用 Stripe 订阅调度 API,创建两阶段执行:当前计划持续至周期结束,然后自动启动新计划,且不会按比例计费。

scheduleAtPeriodEndtrue 时:

  • 订阅计划 不会更改,直到计费周期结束 —— 仅存储 stripeScheduleId 以便客户端检测待处理更改
  • 不会重定向到 Stripe Checkout 或计费门户,更改在服务器端应用
  • 在计费周期结束时,Stripe 会触发 customer.subscription.updated webhook,自动更新订阅记录
  • 如果在周期结束前请求新的升级或计划调度,将首先释放现有的待处理调度

列出活跃订阅

获取用户的活跃订阅:

GET/subscription/list
const { data: subscriptions, error } = await authClient.subscription.list({    query: {        referenceId: '123',        customerType,    },});// 获取活跃订阅const activeSubscription = subscriptions.find(    sub => sub.status === "active" || sub.status === "trialing");// 检查订阅限额const projectLimit = subscriptions?.limits?.projects || 0;
Parameters
referenceIdstring

要列出的订阅的引用 ID。

customerType"user" | "organization"

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

确保在插件配置中加入了 authorizeReference 以授权引用 ID:

auth.ts
stripe({
    // ... 其他选项
    subscription: {
        // ... 其他订阅配置
        authorizeReference: async ({ user, session, referenceId, action }) => {
            if(action === "list-subscription") {
                const org = await db.member.findFirst({
                    where: {
                        organizationId: referenceId,
                        userId: user.id
                    }   
                });
                return org?.role === "owner"
            }
            // 检查用户是否有权限列出此引用的订阅
            return true;
        }
    }
})

取消订阅

取消订阅:

POST/subscription/cancel
const { data, error } = await authClient.subscription.cancel({    referenceId: 'org_123',    customerType,    subscriptionId: 'sub_123',    returnUrl: '/account', // required});
Parameters
referenceIdstring

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

customerType"user" | "organization"

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

subscriptionIdstring

要取消的订阅 ID。

returnUrlstringrequired

点击计费门户返回链接时要跳转的 URL。

将重定向用户进入 Stripe 账单门户,让用户自行取消订阅。

理解取消状态

Stripe 支持不同类型的取消,插件会追踪所有状态:

字段描述
cancelAtPeriodEnd订阅是否(如果状态为 active)或已(如果状态为 canceled)在当前计费周期结束时取消
cancelAt如果订阅计划被取消,这是取消生效的时间
canceledAt如果订阅已被取消,这是取消的时间
endedAt如果订阅已结束,这是订阅结束的日期
status仅在订阅实际结束后更改为 "canceled"

恢复订阅

注意: 仅适用于仍处于活跃状态但有挂起取消或计划更改的订阅。无法恢复已结束的订阅(status: "canceled" 且已设置 endedAt)。

如果用户在取消订阅或计划调度后改变主意,您可以恢复订阅:

POST/subscription/restore
const { data, error } = await authClient.subscription.restore({    referenceId: '123',    customerType,    subscriptionId: 'sub_123',});
Parameters
referenceIdstring

要恢复的订阅的引用 ID。默认为基于 customerType 的值。

customerType"user" | "organization"

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

subscriptionIdstring

要恢复的订阅 ID。

此端点处理两种情况:

  • 挂起取消:设置 cancelAtPeriodEndfalse 并清除 cancelAt / canceledAt,订阅将继续自动续订
  • 挂起计划更改(通过 scheduleAtPeriodEnd):释放 Stripe 订阅计划并清除 stripeScheduleId,当前计划保持不变

创建计费门户会话

要创建一个 Stripe 计费门户会话,让客户可以管理其订阅、更新支付方式和查看账单历史记录:

POST/subscription/billing-portal
const { data, error } = await authClient.subscription.billingPortal({    locale,    referenceId: "123",    customerType,    returnUrl,    disableRedirect: false,});
Parameters
localestring

客户门户显示的语言环境 IETF 标签。 如果未提供或设置为 "auto",则使用浏览器语言环境。

referenceIdstring

订阅的引用 ID。

customerType"user" | "organization"

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

returnUrlstring

退出计费门户后重定向回的 URL。

disableRedirectboolean

禁用自动重定向到计费页面。 @default false

有关支持的语言环境,请参阅 IETF 语言标签文档

此端点创建 Stripe 计费门户会话,并在响应中以 data.url 返回。您可以重定向用户到此地址,让他们管理订阅、支付方式和账单历史记录。

该接口会创建 Stripe 账单门户会话,响应中以 data.url 返回,可以重定向用户至该地址进行订阅和支付管理。

引用系统

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

client.ts
// 为组织创建订阅
await authClient.subscription.upgrade({
    plan: "pro",
    referenceId: "org_123456",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
    seats: 5 // 团队计划席位数量
});

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

团队订阅与席位数量

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

await authClient.subscription.upgrade({
    plan: "team",
    referenceId: "org_123456",
    seats: 10, // 10 个团队成员
    successUrl: "/org/billing/success",
    cancelUrl: "/org/billing"
});

seats 参数将传递给 Stripe 作为订阅品项数量,可用于应用逻辑中限制团队成员数量。

要授权引用 ID,请实现 authorizeReference

auth.ts
subscription: {
    // ... 其他选项
    authorizeReference: async ({ user, session, referenceId, action }) => {
        // 检查用户是否有权限管理此引用的订阅
        if (action === "upgrade-subscription" || action === "cancel-subscription" || action === "restore-subscription") {
            const org = await db.member.findFirst({
                where: {
                    organizationId: referenceId,
                    userId: user.id
                }   
            });
            return org?.role === "owner"
        }
        return true;
    }
}

Webhook 处理

插件自动处理常见 webhook 事件:

  • checkout.session.completed: 结账完成后更新订阅状态
  • customer.subscription.created: 在结账流程外创建订阅时创建订阅
  • customer.subscription.updated: 订阅更改时更新订阅详情
  • customer.subscription.deleted: 标记订阅为已取消

您也可以响应自定义事件:

auth.ts
stripe({
    // ... 其他选项
    onEvent: async (event) => {
        // 处理任何 Stripe 事件
        switch (event.type) {
            case "invoice.paid":
                // 处理已付款发票
                break;
            case "payment_intent.succeeded":
                // 处理支付成功
                break;
        }
    }
})

订阅生命周期钩子

您可以挂载订阅各种生命周期事件的回调:

auth.ts
subscription: {
    // ... 其他选项
    onSubscriptionComplete: async ({ event, subscription, stripeSubscription, plan }) => {
        // 结账方式成功创建订阅时调用
        await sendWelcomeEmail(subscription.referenceId, plan.name);
    },
    onSubscriptionCreated: async ({ event, subscription, stripeSubscription, plan }) => {
        // 订阅在结账外创建时调用(比如后台仪表盘)
        await sendSubscriptionCreatedEmail(subscription.referenceId, plan.name);
    },
    onSubscriptionUpdate: async ({ event, subscription, stripeSubscription }) => {
        // 订阅更新时调用。使用 `stripeSubscription` 获取原始 Stripe 字段,例如 `cancellation_details`。
        console.log(`订阅 ${subscription.id} 已更新`);
    },
    onSubscriptionCancel: async ({ event, subscription, stripeSubscription, cancellationDetails }) => {
        // 订阅取消时调用
        await sendCancellationEmail(subscription.referenceId);
    },
    onSubscriptionDeleted: async ({ event, subscription, stripeSubscription }) => {
        // 订阅删除时调用
        console.log(`订阅 ${subscription.id} 已删除`);
    }
}

试用期

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

auth.ts
{
    name: "pro",
    priceId: "price_0987654321",
    freeTrial: {
        days: 14,
        onTrialStart: async (subscription) => {
            // 试用期开始时调用
            await sendTrialStartEmail(subscription.referenceId);
        },
        onTrialEnd: async ({ subscription }, ctx) => {
            // 试用期结束时调用
            await sendTrialEndEmail(subscription.referenceId);
        },
        onTrialExpired: async (subscription, ctx) => {
            // 试用到期未转换时调用
            await sendTrialExpiredEmail(subscription.referenceId);
        }
    }
}

架构

该 Stripe 插件会向您的数据库添加以下数据表:

用户

表名:user

Table
字段
类型
描述
stripeCustomerId ?
string
-
The Stripe customer ID

组织

表名:organization (仅当 organization.enabledtrue 时有效)

Table
字段
类型
描述
stripeCustomerId ?
string
-
The Stripe customer ID for the organization

订阅

表名:subscription

Table
字段
类型
描述
id
string
PK
Unique identifier for each subscription
plan
string
-
The name of the subscription plan
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.
stripeCustomerId ?
string
-
The Stripe customer ID
stripeSubscriptionId ?
string
-
The Stripe subscription ID
status
string
-
The status of the subscription (active, canceled, etc.)
periodStart ?
Date
-
Start date of the current billing period
periodEnd ?
Date
-
End date of the current billing period
cancelAtPeriodEnd ?
boolean
-
Whether the subscription will be canceled at the end of the period
cancelAt ?
Date
-
If the subscription is scheduled to be canceled, this is the time at which the cancellation will take effect
canceledAt ?
Date
-
If the subscription has been canceled, this is the time when the cancellation was requested. Note: If the subscription was canceled with cancelAtPeriodEnd, this reflects the cancellation request time, not when the subscription actually ends
endedAt ?
Date
-
If the subscription has ended, this is the date the subscription ended
seats ?
number
-
Number of seats for team plans
trialStart ?
Date
-
Start date of the trial period
trialEnd ?
Date
-
End date of the trial period
billingInterval ?
string
-
The billing interval of the subscription (e.g. 'month', 'year')
stripeScheduleId ?
string
-
Stripe Subscription Schedule ID, present when a scheduled plan change is pending

自定义 Schema

若需要更改表名或字段,可传入 schema 选项至 Stripe 插件:

auth.ts
stripe({
    // ... 其他选项
    schema: {
        subscription: {
            modelName: "stripeSubscriptions", // 将订阅表映射为 stripeSubscriptions
            fields: {
                plan: "planName" // 将 plan 字段映射为 planName
            }
        }
    }
})

选项

选项类型说明
stripeClientStripeStripe 客户端实例。必需
stripeWebhookSecretstringStripe webhook 签名密钥。必需
createCustomerOnSignUpboolean用户注册时是否自动创建 Stripe 客户。默认:false
onCustomerCreatefunction客户创建后回调,参数 { stripeCustomer, user } 和上下文。
getCustomerCreateParamsfunction自定义 Stripe 客户创建参数,参数是 user 和上下文。
onEventfunction收到任何 Stripe webhook 事件时回调,参数为 Stripe.Event
subscriptionobject订阅配置。详见下文 订阅选项
organizationobject启用组织客户支持。详见下文 组织选项
schemaobject自定义 Stripe 插件的数据库 schema。

订阅选项

OptionTypeDescription
enabledboolean是否启用订阅功能。必需。
plansStripePlan[] or function订阅计划数组,或返回计划的异步函数。启用时必需
requireEmailVerificationboolean在允许订阅升级前是否要求邮箱验证。默认:false
authorizeReferencefunction授权 reference ID。接收 { user, session, referenceId, action } 以及上下文。
getCheckoutSessionParamsfunction自定义 Stripe Checkout 会话参数。接收 { user, session, plan, subscription }、请求和上下文。
onSubscriptionCompletefunction通过 checkout 创建订阅时调用。接收 { event, stripeSubscription, subscription, plan } 和上下文。
onSubscriptionCreatedfunction在 checkout 之外创建订阅时调用。接收 { event, stripeSubscription, subscription, plan }
onSubscriptionUpdatefunction订阅更新时调用。接收 { event, subscription, stripeSubscription }。像 cancellation_details 这类原始 Stripe 字段请使用 stripeSubscription
onSubscriptionCancelfunction订阅取消时调用。接收 { event, subscription, stripeSubscription, cancellationDetails }
onSubscriptionDeletedfunction订阅删除时调用。接收 { event, stripeSubscription, subscription }

计划配置

OptionTypeDescription
namestring计划名称。必需。
priceIdstringStripe price ID。除非使用 lookupKey,否则为必需
lookupKeystringStripe price lookup key。可替代 priceId
annualDiscountPriceIdstring年付账单的 price ID。
annualDiscountLookupKeystring年付账单的 Stripe price lookup key。
limitsobject计划限制(例如 { projects: 10, storage: 5 })。
groupstring用于对计划分组的组名。
seatPriceIdstring按席位计费的 price ID。需要 organization 插件。
prorationBehaviorstring订阅更新时的按比例计费行为:"create_prorations"(默认)、"always_invoice""none"
lineItemsLineItem[]在 checkout 会话中包含的附加条目。
freeTrialobject试用配置。见下文

Stripe 不支持通过 Checkout Sessions 进行 混合计费周期订阅。checkout 中的所有条目都应使用相同的计费周期(例如全部按月或全部按年)。如果周期不同,Stripe API 会拒绝该请求。

免费试用配置

OptionTypeDescription
daysnumber试用天数。必需。
onTrialStartfunction试用开始时调用。接收 subscription
onTrialEndfunction试用结束时调用。接收 { subscription } 和上下文。
onTrialExpiredfunction试用在未转化时过期调用。接收 subscription 和上下文。

组织选项

OptionTypeDescription
enabledboolean启用组织客户支持。必需。
getCustomerCreateParamsfunction自定义组织的 Stripe 客户创建参数。接收 organization 和上下文。
onCustomerCreatefunction组织客户创建后调用。接收 { stripeCustomer, organization } 和上下文。

高级用法

与组织插件一起使用

Stripe 插件集成了 organization 插件,支持组织作为 Stripe 客户。订阅的计费对象由个人用户转为组织,适合 B2B 场景。

启用组织客户时:

  • 当组织首次订阅时会自动创建 Stripe Customer
  • 组织名称变更会同步到 Stripe Customer
  • 有活跃订阅的组织无法被删除

启用组织客户

设置 organization.enabledtrue,并确保已安装组织插件:

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

创建组织订阅

即使启用了组织客户,用户订阅仍可用且为默认。若需组织作为计费客户,传入 customerType: "organization"

client.ts
await authClient.subscription.upgrade({
    plan: "team",
    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";
    }
}

组织计费邮箱

组织没有唯一邮箱,账单邮箱不会自动同步。组织一般使用专门账单邮箱,和用户账户不同。 你可在结账后通过 Stripe 仪表盘修改邮箱,或用 stripeClient 自定义:

await stripeClient.customers.update(organization.stripeCustomerId, {
    email: "billing@company.com"
});

处理用户删除

有活跃订阅的组织会被自动阻止删除,但用户不会。若要在用户删除时实现相同行为,可在订阅活跃时于 beforeDelete 回调中抛出错误:

auth.ts
import { betterAuth } from "better-auth";
import { APIError } from "better-auth/api";

export const auth = betterAuth({
    user: {
        deleteUser: {
            enabled: true,
            beforeDelete: async (user) => {
                if (!user.stripeCustomerId) return;

                for await (const sub of stripeClient.subscriptions.list({
                    customer: user.stripeCustomerId,
                    status: "all",
                })) {
                    if (["canceled", "incomplete", "incomplete_expired"].includes(sub.status)) continue;

                    throw new APIError("BAD_REQUEST", {
                        message: "请在删除账户前取消当前活跃订阅",
                    });
                    // 或立即取消:await stripeClient.subscriptions.cancel(sub.id);
                    // 或在周期结束时取消:      await stripeClient.subscriptions.update(sub.id, { cancel_at_period_end: true });
                }
            },
        },
    },
});

自定义 Checkout Session 参数

你可以传递额外参数给 Stripe Checkout:

auth.ts
getCheckoutSessionParams: async ({ user, session, plan, subscription }, ctx) => {
    return {
        params: {
            allow_promotion_codes: true,
            tax_id_collection: {
                enabled: true
            },
            billing_address_collection: "required",
            custom_text: {
                submit: {
                    message: "我们将立即为您启用订阅"
                }
            },
            metadata: {
                planType: "business",
                referralCode: user.metadata?.referralCode
            }
        },
        options: {
            idempotencyKey: `sub_${user.id}_${plan.name}_${Date.now()}`
        }
    };
}

税务收集

开启税务 ID 收集:

auth.ts
subscription: {
    // ... 其他选项
    getCheckoutSessionParams: async ({ user, session, plan, subscription }, ctx) => {
        return {
            params: {
                tax_id_collection: {
                    enabled: true
                }
            }
        };
    }
}

自动税费计算

启用自动税费计算(按客户位置),设置 automatic_tax

auth.ts
subscription: {
    // ... 其他选项
    getCheckoutSessionParams: async ({ user, session, plan, subscription }, ctx) => {
        return {
            params: {
                automatic_tax: {
                    enabled: true
                }
            }
        };
    }
}

注意需先在 Stripe 仪表盘设置税务注册和配置。

试用期管理

工作原理:

  • 系统会跟踪每个用户在所有计划中的试用使用情况
  • 当用户订阅带试用期的计划时,系统会检查其订阅历史
  • 如果用户曾经使用过试用期(由 trialStart/trialEnd 字段或 trialing 状态表示),则不会再提供新的试用期
  • 这可防止用户通过取消订阅再重新订阅来多次获取免费试用

示例场景:

  1. 用户订阅“Starter”计划,有 7 天试用
  2. 用户试用后取消订阅
  3. 用户尝试订阅“Premium”计划,不会再提供试用
  4. 用户将立即支付 Premium 计划费用

示例:

  1. 用户订阅“Starter”计划,有 7 天试用
  2. 用户试用后取消订阅
  3. 用户尝试订阅“Premium”计划,试用被禁止
  4. 用户即刻支付 Premium 计划费用

此行为自动生效,无需额外配置,且不可通过配置覆盖。

故障排查

Webhook 问题

若 Webhook 未正确处理:

  1. 确认 Stripe 仪表盘中配置的 Webhook URL 是否正确
  2. 验证 Webhook 签名密钥是否正确
  3. 检查是否勾选了全部必需事件
  4. 查看服务器日志是否有处理错误

订阅状态异常

若订阅状态未正确更新:

  1. 确认 Webhook 事件已接收并处理
  2. 确认 stripeCustomerIdstripeSubscriptionId 是否正确填写
  3. 核对应用中引用 ID 是否与 Stripe 一致

本地测试 Webhook

可使用 Stripe CLI 将 Webhook 转发至本地:

stripe listen --forward-to localhost:3000/api/auth/stripe/webhook

CLI 会输出本地使用的 Webhook 签名密钥,将其配置至本地环境即可。