Stripe

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

此插件目前处于测试阶段。我们正在积极收集反馈并探索额外功能。如果您有功能请求或建议,请加入我们的Discord社区进行讨论。

功能

  • 用户注册时自动创建Stripe客户
  • 管理订阅计划和定价
  • 处理订阅生命周期事件(创建、更新、取消)
  • 通过签名验证安全处理Stripe webhooks
  • 向您的应用程序公开订阅数据
  • 支持试用期和订阅升级
  • 灵活的引用系统,将订阅与用户或组织关联
  • 团队订阅支持,具有席位管理功能

安装

安装插件

首先,安装插件:

npm install @better-auth/stripe

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

安装Stripe SDK

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

npm install stripe

将插件添加到您的auth配置中

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!)
 
export const auth = betterAuth({
    // ... 您现有的配置
    plugins: [
        stripe({
            stripeClient,
            stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
            createCustomerOnSignUp: true,
        })
    ]
})

添加客户端插件

auth-client.ts
import { createAuthClient } from "better-auth/client"
import { stripeClient } from "@better-auth/stripe/client"
 
export const client = createAuthClient({
    // ... 您现有的配置
    plugins: [
        stripeClient({
            subscription: true //如果您想启用订阅管理
        })
    ]
})

迁移数据库

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

npx @better-auth/cli migrate

查看Schema部分手动添加表。

设置Stripe webhooks

在您的Stripe控制面板中创建一个指向以下地址的webhook端点:

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

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

确保至少选择这些事件:

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

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

使用方法

客户管理

您可以仅将此插件用于客户管理,而不启用订阅。如果您只想将Stripe客户与您的用户关联起来,这很有用。

默认情况下,当用户注册时,如果您设置了createCustomerOnSignUp: true,将自动创建一个Stripe客户。该客户在您的数据库中与用户关联。 您可以自定义客户创建过程:

auth.ts
stripe({
    // ... 其他选项
    createCustomerOnSignUp: true,
    onCustomerCreate: async ({ customer, stripeCustomer, user }, request) => {
        // 对新创建的客户做些处理
        console.log(`客户 ${customer.id} 已为用户 ${user.id} 创建`);
    },
    getCustomerCreateParams: async ({ user, session }, request) => {
        // 自定义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,
            }
        }
    ]
}
 
// 动态计划(从数据库或API获取)
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方法:

client.ts
await client.subscription.upgrade({
    plan: "pro",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
    annual: true, // 可选:升级到年度计划
    referenceId: "org_123" // 可选:默认为当前登录用户的ID
    seats: 5 // 可选:用于团队计划
});

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

重要: successUrl参数将在内部进行修改,以处理结账完成和webhook处理之间的竞争条件。该插件创建一个中间重定向,确保在重定向到您的成功页面之前正确更新订阅状态。

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

对于每个引用ID(用户或组织),一次只支持一个活动或试用订阅。该插件目前不支持同一引用ID有多个并发活动订阅。

列出活动订阅

要获取用户的活动订阅:

client.ts
const { data: subscriptions } = await client.subscription.list();
 
// 获取活动订阅
const activeSubscription = subscriptions.find(
    sub => sub.status === "active" || sub.status === "trialing"
);
 
// 检查订阅限制
const projectLimit = subscriptions?.limits?.projects || 0;

取消订阅

要取消订阅:

client.ts
const { data } = await client.subscription.cancel({
    returnUrl: "/account",
    referenceId: "org_123" // 可选,默认为userId
});

这将重定向用户到Stripe账单门户,他们可以在那里取消订阅。

恢复已取消的订阅

如果用户在取消订阅后改变主意(但在订阅期结束前),您可以恢复订阅:

client.ts
const { data } = await client.subscription.restore({
    referenceId: "org_123" // 可选,默认为userId
});

这将重新激活先前设置为在计费期结束时取消的订阅(cancelAtPeriodEnd: true)。订阅将继续自动续订。

注意: 这仅适用于仍然活动但标记为在期末取消的订阅。它不能恢复已经结束的订阅。

引用系统

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

client.ts
// 为组织创建订阅
await client.subscription.upgrade({
    plan: "pro",
    referenceId: "org_123456",
    successUrl: "/dashboard",
    cancelUrl: "/pricing",
    seats: 5 // 团队计划的席位数
});
 
// 列出组织的订阅
const { data: subscriptions } = await client.subscription.list({
    query: {
        referenceId: "org_123456"
    }
});

带有席位的团队订阅

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

await client.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.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);
    },
    onSubscriptionUpdate: async ({ event, subscription }) => {
        // 订阅更新时调用
        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, user }, request) => {
            // 试用结束时调用
            await sendTrialEndEmail(user.email);
        },
        onTrialExpired: async (subscription) => {
            // 试用过期且未转换时调用
            await sendTrialExpiredEmail(subscription.referenceId);
        }
    }
}

Schema

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

用户

表名: user

Field NameTypeKeyDescription
stripeCustomerIdstring-Stripe客户ID

订阅

表名: subscription

Field NameTypeKeyDescription
idstring每个订阅的唯一标识符
planstring-订阅计划的名称
referenceIdstring-此订阅关联的ID(默认为用户ID)
stripeCustomerIdstringStripe客户ID
stripeSubscriptionIdstringStripe订阅ID
statusstring-订阅状态(活动、已取消等)
periodStartDate当前计费期的开始日期
periodEndDate当前计费期的结束日期
cancelAtPeriodEndboolean订阅是否将在计费期结束时取消
seatsnumber团队计划的席位数
trialStartDate试用期的开始日期
trialEndDate试用期的结束日期

自定义Schema

要更改schema表名或字段,您可以向Stripe插件传递schema选项:

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

选项

主要选项

stripeClient: Stripe - Stripe客户端实例。必需。

stripeWebhookSecret: string - 来自Stripe的webhook签名密钥。必需。

createCustomerOnSignUp: boolean - 用户注册时是否自动创建Stripe客户。默认: false

onCustomerCreate: (data: { customer: Customer, stripeCustomer: Stripe.Customer, user: User }, request?: Request) => Promise<void> - 客户创建后调用的函数。

getCustomerCreateParams: (data: { user: User, session: Session }, request?: Request) => Promise<{}> - 自定义Stripe客户创建参数的函数。

onEvent: (event: Stripe.Event) => Promise<void> - 任何Stripe webhook事件调用的函数。

订阅选项

enabled: boolean - 是否启用订阅功能。必需。

plans: Plan[] | (() => Promise<Plan[]>) - 订阅计划数组或返回计划的函数。启用订阅时必需。

requireEmailVerification: boolean - 是否在允许订阅升级前要求验证电子邮件。默认: false

authorizeReference: (data: { user: User, session: Session, referenceId: string, action: "upgrade-subscription" | "list-subscription" | "cancel-subscription" | "restore-subscription"}, request?: Request) => Promise<boolean> - 授权引用ID的函数。

计划配置

每个计划可以具有以下属性:

name: string - 计划名称。必需。

priceId: string - Stripe价格ID。除非使用lookupKey,否则必需。

lookupKey: string - Stripe价格查找键。作为priceId的替代选项。

annualDiscountPriceId: string - 带有折扣的年度账单价格ID。

limits: Record<string, number> - 与计划相关的限制(例如,{ projects: 10, storage: 5 })。

group: string - 计划的组名,用于对计划进行分类。

freeTrial: 包含试用配置的对象:

  • days: number - 试用天数。
  • onTrialStart: (subscription: Subscription) => Promise<void> - 试用开始时调用。
  • onTrialEnd: (data: { subscription: Subscription, user: User }, request?: Request) => Promise<void> - 试用结束时调用。
  • onTrialExpired: (subscription: Subscription) => Promise<void> - 试用过期且未转换时调用。

高级用法

与组织一起使用

Stripe插件与组织插件配合良好。您可以将订阅与组织而非个人用户关联:

client.ts
// 获取活动组织
const { data: activeOrg } = client.useActiveOrganization();
 
// 为组织创建订阅
await client.subscription.upgrade({
    plan: "team",
    referenceId: activeOrg.id,
    seats: 10,
    annual: true, // 升级到年度计划(可选)
    successUrl: "/org/billing/success",
    cancelUrl: "/org/billing"
});

确保实现authorizeReference函数,以验证用户是否有权为组织管理订阅:

auth.ts
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结账会话:

auth.ts
getCheckoutSessionParams: async ({ user, session, plan, subscription }, request) => {
    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()}`
        }
    };
}

税收收集

要启用税收收集:

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

故障排除

Webhook问题

如果webhooks未正确处理:

  1. 检查您的webhook URL是否在Stripe控制面板中正确配置
  2. 验证webhook签名密钥是否正确
  3. 确保您在Stripe控制面板中选择了所有必要的事件
  4. 检查服务器日志中是否有webhook处理期间的任何错误

订阅状态问题

如果订阅状态未正确更新:

  1. 确保接收并处理webhook事件
  2. 检查stripeCustomerIdstripeSubscriptionId字段是否正确填充
  3. 验证您的应用程序和Stripe之间的引用ID是否匹配

本地测试Webhooks

对于本地开发,您可以使用Stripe CLI将webhooks转发到您的本地环境:

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

这将为您提供一个webhook签名密钥,您可以在本地环境中使用。