Creem
使用 Creem 的支付和订阅管理的更好认证插件
Creem 是一个金融操作系统,帮助团队和个人在全球销售软件时无税务合规难题地分账收入并协作处理财务工作流。此插件将 Creem 与 Better Auth 集成,将支付处理及订阅管理直接嵌入您的认证层。
此插件由 Creem 团队维护。如遇错误、问题或功能请求, 请访问 Creem GitHub 仓库。
在 Creem Discord 或应用内实时聊天中获得支持
需要帮助?随时在 Discord 上联系我们的团队。
功能特性
- 数据库持久化 - 自动同步客户和订阅数据到您的数据库
- 访问管理 - 根据订阅状态自动授予或撤销用户访问权限
- 客户同步 - 将 Creem 客户 ID 与数据库中的用户同步
- 结账集成 - 为已认证用户自动创建结账会话,或为未认证用户手动创建
- 客户门户 - 允许用户管理订阅、查看发票和更新支付方式
- 订阅管理 - 取消、检索和跟踪订阅详情(适用于已认证或未认证用户)
- 交易历史 - 为已认证用户搜索和过滤交易记录,或为未认证用户手动操作
- Webhook 处理 - 通过签名验证安全处理 Creem Webhook
- 灵活架构 - 使用 Better Auth 端点或直接使用服务端函数
- 试用防滥用 - 每个账户在所有计划中只能获得一次试用(使用数据库模式时)
安装
获取 API Key
在 Creem 控制面板 的“Developers”菜单下获取您的 API Key,并将其添加到环境变量中:
# .env
CREEM_API_KEY=your_api_key_here测试模式和生产环境使用不同的 API Key,请确保所用的密钥对应正确的环境。
配置
服务端配置
配置 Better Auth 并使用 Creem 插件:
// lib/auth.ts
import { betterAuth } from "better-auth";
import { creem } from "@creem_io/better-auth";
export const auth = betterAuth({
database: {
// 您的数据库配置
},
plugins: [
creem({
apiKey: process.env.CREEM_API_KEY!,
webhookSecret: process.env.CREEM_WEBHOOK_SECRET, // 可选,传入签名密钥后自动启用 webhook
testMode: true, // 可选,开发时使用测试模式
defaultSuccessUrl: "/success", // 可选,支付成功后的跳转地址
persistSubscriptions: true, // 可选,启用数据库持久化(默认:true)
}),
],
});客户端配置
标准配置
// lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { creemClient } from "@creem_io/better-auth/client";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL,
plugins: [creemClient()],
});增强的 TypeScript 支持(仅限 React)
获得更好的 TypeScript 智能提示和自动补全:
// lib/auth-client.ts
import { createCreemAuthClient } from "@creem_io/better-auth/create-creem-auth-client";
import { creemClient } from "@creem_io/better-auth/client";
export const authClient = createCreemAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL,
plugins: [creemClient()],
});createCreemAuthClient 包装方法提供了增强的 TypeScript 支持和更简洁的参数类型,针对 Creem 插件进行了优化。
数据库迁移
如果启用数据库持久化(persistSubscriptions: true),请生成并运行数据库模式:
npx auth migratenpx auth generate根据您使用的数据库适配器,可能需要额外的设置。请参阅 Better Auth 适配器文档 了解详情。
Webhook 设置
创建 Webhook 端点
在您的 Creem 控制面板中新建一个 webhook 端点,指向您的本地或生产服务器地址:
https://your-domain.com/api/auth/creem/webhook(/api/auth 是 Better Auth 服务端默认路径)
如果本地开发,请参阅步骤 3。
配置 Webhook 密钥
复制 Creem 提供的 webhook 签名密钥,并添加到环境变量:
CREEM_WEBHOOK_SECRET=your_webhook_secret_here更新服务端配置:
creem({
apiKey: process.env.CREEM_API_KEY!,
webhookSecret: process.env.CREEM_WEBHOOK_SECRET,
testMode: true,
})数据库模式
启用 persistSubscriptions: true 时,插件创建以下数据表:
Creem 订阅表
表名:creem_subscription
| 字段 | 类型 | 描述 |
|---|---|---|
id | string | 主键 |
productId | string | Creem 产品 ID |
referenceId | string | 您的用户或组织 ID |
creemCustomerId | string | Creem 客户 ID |
creemSubscriptionId | string | Creem 订阅 ID |
creemOrderId | string | Creem 订单 ID |
status | string | 订阅状态 |
periodStart | date | 账单周期起始日期 |
periodEnd | date | 账单周期结束日期 |
cancelAtPeriodEnd | boolean | 是否订阅将在周期结束时取消 |
用户表扩展字段
| 字段 | 类型 | 描述 |
|---|---|---|
creemCustomerId | string | 关联用户到 Creem 客户 |
使用说明
结账
创建结账会话以处理支付:
"use client";
import { authClient } from "@/lib/auth-client";
export function SubscribeButton({ productId }: { productId: string }) {
const handleCheckout = async () => {
const { data, error } = await authClient.creem.createCheckout({
productId,
successUrl: "/dashboard",
discountCode: "LAUNCH50", // 可选
metadata: { planType: "pro" }, // 可选
});
if (data?.url) {
window.location.href = data.url;
}
};
return <button onClick={handleCheckout}>立即订阅</button>;
}结账参数说明
productId(required) - The Creem product IDunits- Number of units (default: 1)successUrl- Redirect URL after successful paymentdiscountCode- Discount code to applycustomer- Customer information (auto-populated from session)metadata- Additional metadata (auto-includes user ID asreferenceId)requestId- Idempotency key for duplicate prevention
客户门户
重定向用户管理其订阅:
const handlePortal = async () => {
// 无需跳转,门户将在同一标签页打开
const { data, error } = await authClient.creem.createPortal();
};订阅管理
取消订阅
启用数据库持久化时,将自动根据已认证用户查找订阅:
const handleCancel = async () => {
const { data, error } = await authClient.creem.cancelSubscription();
if (data?.success) {
console.log(data.message);
}
};禁用数据库持久化时,需提供订阅 ID:
const { data } = await authClient.creem.cancelSubscription({
id: "sub_123456",
});获取订阅详情
获取已认证用户的订阅详情:
const getSubscription = async () => {
const { data } = await authClient.creem.retrieveSubscription();
if (data) {
console.log(`状态: ${data.status}`);
console.log(`产品: ${data.product.name}`);
console.log(`价格: ${data.product.price} ${data.product.currency}`);
}
};检查访问权限
确认用户是否有有效订阅(需数据库模式):
const { data } = await authClient.creem.hasAccessGranted();
if (data?.hasAccess) {
// 用户拥有有效订阅权限
console.log(`过期时间: ${data.expiresAt}`);
}此函数判断用户是否在当前账期内享有访问权限。例如,用户购买了一年制订阅,取消后仍可访问直到年期结束。
交易历史
搜索已认证用户的交易记录:
const { data } = await authClient.creem.searchTransactions({
productId: "prod_xyz789", // 可选筛选
pageNumber: 1,
pageSize: 50,
});
if (data?.transactions) {
data.transactions.forEach((tx) => {
console.log(`${tx.type}: ${tx.amount} ${tx.currency}`);
});
}Webhook 处理
插件提供灵活的 webhook 处理方案,支持细粒度事件处理及高阶访问控制处理器。
高阶访问控制处理器(推荐)
这些处理器提供最简单且强大的用户访问管理方式。它们自动处理所有支付场景及订阅状态,无需单独管理每个订阅事件。
Database Persistence Required: These handlers require the database persistence option to be enabled in your plugin configuration.
| 处理器名称 | 数据参数类型 | 描述 |
|---|---|---|
onGrantAccess | GrantAccessContext | 当应授予用户访问权限时调用。 处理成功的支付、活动订阅和试用期。使用此功能启用功能、添加用户到组或更新权限。 |
onRevokeAccess | RevokeAccessContext | 当应撤销用户访问权限时调用。 处理取消、到期、退款和支付失败。使用此功能禁用功能、从组中移除或撤销权限。 |
为什么使用这些处理器?
- 访问控制的单一真实来源
- 自动处理所有支付场景
- 减少代码复杂性和潜在错误
- 适用于一次性购买和订阅
- 考虑当前计费周期和访问过期日期
// lib/auth.ts
import { betterAuth } from "better-auth";
import { creem } from "@creem_io/better-auth";
export const auth = betterAuth({
database: {
// 您的数据库配置
},
plugins:[
creem({
apiKey: process.env.CREEM_API_KEY!,
webhookSecret: process.env.CREEM_WEBHOOK_SECRET!,
onGrantAccess: async ({ reason, product, customer, metadata }) => {
const userId = metadata?.referenceId as string;
// 根据您的数据库逻辑更新
await db.user.update({
where: { id: userId },
data: {
hasAccess: true,
subscriptionTier: product.name,
accessReason: reason
},
});
console.log(`已授予 ${customer.email} ${reason} 访问权限`);
},
onRevokeAccess: async ({ reason, product, customer, metadata }) => {
const userId = metadata?.referenceId as string;
// 根据您的数据库逻辑更新
await db.user.update({
where: { id: userId },
data: {
hasAccess: false,
revokeReason: reason
},
});
console.log(`已撤销 ${customer.email} 访问权限(原因:${reason})`);
},
}),
],
});授权访问的原因
subscription_active- 订阅处于活动状态subscription_trialing- 订阅处于试用期内subscription_paid- 订阅付款已收到
撤销访问的原因
subscription_paused- 订阅被用户或管理员暂停subscription_expired- 订阅到期未续订subscription_period_end- 当前订阅周期结束未续订
细粒度事件处理器
高级用例需要对特定事件进行精细控制时,使用以下处理器:
| 处理器名称 | 数据参数类型 | 描述 |
|---|---|---|
onCheckoutCompleted | FlatCheckoutCompleted | 结账成功完成时调用。 |
onRefundCreated | FlatRefundCreated | 为付款生成退款时触发。 |
onDisputeCreated | FlatDisputeCreated | 创建付款争议/拒付时调用。 |
onSubscriptionActive | FlatSubscriptionEvent | 订阅变为活动状态时触发。 |
onSubscriptionTrialing | FlatSubscriptionEvent | 订阅进入试用期时触发。 |
onSubscriptionCanceled | FlatSubscriptionEvent | 订阅被取消时调用。 |
onSubscriptionPaid | FlatSubscriptionEvent | 订阅付款已收到时触发。 |
onSubscriptionExpired | FlatSubscriptionEvent | 订阅已过期(未续订/未付款)时触发。 |
onSubscriptionUnpaid | FlatSubscriptionEvent | 订阅付款失败或未付款时触发。 |
onSubscriptionUpdate | FlatSubscriptionEvent | 订阅设置/详情更新时触发。 |
onSubscriptionPastDue | FlatSubscriptionEvent | 订阅付款逾期或拖欠时触发。 |
onSubscriptionPaused | FlatSubscriptionEvent | 订阅被用户或管理员暂停时触发。 |
如何使用 Webhook 处理器
以展平成易访问的形式处理单个 webhook 事件:
// lib/auth.ts
import { betterAuth } from "better-auth";
import { creem } from "@creem_io/better-auth";
export const auth = betterAuth({
database: {
// 您的数据库配置
},
plugins: [
creem({
apiKey: process.env.CREEM_API_KEY!,
webhookSecret: process.env.CREEM_WEBHOOK_SECRET!,
onCheckoutCompleted: async (data) => {
const { customer, product, order, webhookEventType } = data;
console.log(`${customer.email} 购买了 ${product.name}`);
// 适合一次性支付场景
await sendThankYouEmail(customer.email);
},
onSubscriptionActive: async (data) => {
const { customer, product, status } = data;
// 处理激活订阅逻辑
},
onSubscriptionTrialing: async (data) => {
// 处理试用期逻辑
},
onSubscriptionCanceled: async (data) => {
// 处理取消逻辑
},
onSubscriptionExpired: async (data) => {
// 处理过期逻辑
},
onRefundCreated: async (data) => {
// 处理退款逻辑
},
onDisputeCreated: async (data) => {
// 处理争议逻辑
},
}),
],
});自定义 Webhook 处理器
自行创建具备签名验证的 webhook 端点:
// app/api/webhooks/custom/route.ts
import { validateWebhookSignature } from "@creem_io/better-auth/server";
export async function POST(req: Request) {
const payload = await req.text();
const signature = req.headers.get("creem-signature");
if (
!validateWebhookSignature(
payload,
signature,
process.env.CREEM_WEBHOOK_SECRET!
)
) {
return new Response("签名无效", { status: 401 });
}
const event = JSON.parse(payload);
// 您的自定义 webhook 处理逻辑
return Response.json({ received: true });
}服务端函数
可以直接在服务器组件、服务端动作或 API 路由中调用这些工具函数,无需通过 Better Auth 端点。
导入服务端工具
import {
createCheckout,
createPortal,
cancelSubscription,
retrieveSubscription,
searchTransactions,
checkSubscriptionAccess,
isActiveSubscription,
formatCreemDate,
getDaysUntilRenewal,
validateWebhookSignature,
} from "@creem_io/better-auth/server";服务端组件示例
import { checkSubscriptionAccess } from "@creem_io/better-auth/server";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user) {
redirect("/login");
}
const status = await checkSubscriptionAccess(
{
apiKey: process.env.CREEM_API_KEY!,
testMode: true,
},
{
database: auth.options.database,
userId: session.user.id,
}
);
if (!status.hasAccess) {
redirect("/subscribe");
}
return (
<div>
<h1>欢迎来到仪表盘</h1>
<p>订阅状态: {status.status}</p>
{status.expiresAt && (
<p>续订日期: {status.expiresAt.toLocaleDateString()}</p>
)}
</div>
);
}服务端动作示例
"use server";
import { createCheckout } from "@creem_io/better-auth/server";
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
export async function startCheckout(productId: string) {
const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user) {
throw new Error("未认证");
}
const { url } = await createCheckout(
{
apiKey: process.env.CREEM_API_KEY!,
testMode: true,
},
{
productId,
customer: { email: session.user.email },
successUrl: "/success",
metadata: { userId: session.user.id },
}
);
redirect(url);
}中间件示例
基于订阅状态保护路由:
import { checkSubscriptionAccess } from "@creem_io/better-auth/server";
import { auth } from "@/lib/auth";
import { NextRequest, NextResponse } from "next/server";
export async function middleware(request: NextRequest) {
const session = await auth.api.getSession({
headers: request.headers,
});
if (!session?.user) {
return NextResponse.redirect(new URL("/login", request.url));
}
const status = await checkSubscriptionAccess(
{
apiKey: process.env.CREEM_API_KEY!,
testMode: true,
},
{
database: auth.options.database,
userId: session.user.id,
}
);
if (!status.hasAccess) {
return NextResponse.redirect(new URL("/subscribe", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*"],
};工具函数示例
import {
isActiveSubscription,
formatCreemDate,
getDaysUntilRenewal,
} from "@creem_io/better-auth/server";
// 检查状态是否允许访问
if (isActiveSubscription(subscription.status)) {
// 用户有访问权限
}
// 格式化 Creem 时间戳
const renewalDate = formatCreemDate(subscription.next_billing_date);
console.log(renewalDate.toLocaleDateString());
// 计算距离续订还有多少天
const days = getDaysUntilRenewal(subscription.current_period_end_date);
console.log(`距离续订还有 ${days} 天`);数据库模式 vs API 模式
插件支持两种运行模式:
数据库模式(推荐)
当 persistSubscriptions: true(默认)时,订阅数据存储在您的数据库中。
Benefits:
- Fast access checks without API calls
- Offline access to subscription data
- Query subscriptions with SQL
- Automatic synchronization via webhooks
- Trial abuse prevention
用法:
creem({
apiKey: process.env.CREEM_API_KEY!,
persistSubscriptions: true, // 默认开启
})API 模式
当 persistSubscriptions: false,所有数据均直接来自 Creem API。
Benefits:
- No database schema required
- Simpler initial setup
Limitations:
- Requires API call for each access check
- Some features require custom implementation
- No built-in trial abuse prevention
用法:
creem({
apiKey: process.env.CREEM_API_KEY!,
persistSubscriptions: false,
})API 模式下,checkSubscriptionAccess 和 hasAccessGranted 等函数功能受限,可能需直接使用 Creem SDK 实现自定义逻辑。
类型导出说明
服务端类型
| Type Name | Description | Typical Usage |
|---|---|---|
CreemOptions | Configuration options for the Creem plugin, such as API keys and persistence settings. | Used to configure the plugin on the server. |
GrantAccessContext | Context passed to custom access control hooks when granting access to a user. | Used in custom access logic. |
RevokeAccessContext | Context passed to hooks when revoking user access due to subscription status changes. | Used in custom access logic. |
GrantAccessReason | Enum or type describing reasons for granting access (e.g., payment received, trial activated). | Returned in access-related hooks and events. |
RevokeAccessReason | Enum or type describing reasons for revoking access (e.g., canceled, payment failed). | Returned in access-related hooks and events. |
FlatCheckoutCompleted | Event object type for webhook payload when a checkout completes successfully. | Used in webhook handlers and event listeners. |
FlatRefundCreated | Event object type for webhook payload when a refund is created. | Used in webhook handlers and event listeners. |
FlatDisputeCreated | Event object type for webhook payload when a dispute is created. | Used in webhook handlers and event listeners. |
FlatSubscriptionEvent | Event object type for generic subscription events (created, updated, canceled, etc). | Used in webhook handlers and event listeners. |
客户端类型
| Type Name | Description |
|---|---|
CreateCheckoutInput | Input parameters for creating a checkout session. |
CreateCheckoutResponse | Response shape for a checkout session creation request. |
CheckoutCustomer | Customer information type used in a checkout session. |
CreatePortalInput | Input parameters for creating a customer portal session. |
CreatePortalResponse | Response data for a request to create a customer portal. |
CancelSubscriptionInput | Input parameters when cancelling a subscription. |
CancelSubscriptionResponse | Response data for a subscription cancellation request. |
RetrieveSubscriptionInput | Input for retrieving a specific subscription's details. |
SubscriptionData | Subscription information structure as returned by the API. |
SearchTransactionsInput | Filters and parameters for searching transactions. |
SearchTransactionsResponse | Response structure for a transaction search query. |
TransactionData | Data relating to individual transactions (e.g., payment, refund, etc). |
HasAccessGrantedResponse | The shape of the response indicating whether a user has access based on subscription status/rules. |
试用滥用防护
启用数据库模式(persistSubscriptions: true)时,插件将自动防止试用滥用。用户整个账户内所有订阅计划仅能获得一次试用机会。
Example Scenario:
- User subscribes to "Starter" plan with 7-day trial
- User cancels subscription during the trial period
- User attempts to subscribe to "Premium" plan
- No trial is offered - user is charged immediately
该防护为自动且不可配置,试用资格在创建订阅时判断,无法覆盖。
常见问题排查
Webhook 问题
Webhook 无法正确处理时:
- 确认 Creem 控制面板中 webhook URL 是否正确
- 检查 webhook 签名密钥是否匹配
- 确保在 Creem 控制面板中选中所有必要事件
- 查看服务器日志中 webhook 处理错误
- 用 Creem 的 webhook 测试工具测试发送
订阅状态异常
订阅状态未更新时:
- 确认 webhook 是否有正确接收和处理
- 确认
creemCustomerId和creemSubscriptionId字段已填充 - 检查您应用和 Creem 之间的引用 ID 是否一致
- 查看 webhook 处理器日志中是否有错误
数据库模式无效
数据库持久化不生效时:
- 确认启用了
persistSubscriptions: true(默认开启) - 运行迁移命令:
npx auth migrate - 检查数据库连接是否正常
- 确认数据库表模式是否成功创建
- 检查数据库适配器配置
API 模式限制
部分功能仅数据库模式支持或者需额外参数:
checkSubscriptionAccessrequires passing theuserIdparametergetActiveSubscriptionsrequires passing theuserIdparameter- No automatic trial abuse prevention
- No access to
hasAccessGrantedclient method
使用时建议启用数据库模式,或自行用 Creem SDK 实现自定义逻辑。
额外资源
- Creem Documentation
- Creem Dashboard
- Better Auth Documentation
- Plugin GitHub Repository Additional Documentation
支持
遇到问题或有疑问:
- Open an issue on GitHub
- Contact Creem support at support@creem.io
- Join our Discord community for real-time support and discussion.
- Chat with us directly using the in-app live chat on the Creem dashboard.