Creem

使用 Creem 的支付和订阅管理的更好认证插件

Creem 是一个金融操作系统,帮助团队和个人在全球销售软件时无税务合规难题地分账收入并协作处理财务工作流。此插件将 Creem 与 Better Auth 集成,将支付处理及订阅管理直接嵌入您的认证层。

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

在 Creem Discord 或应用内实时聊天中获得支持

需要帮助?随时在 Discord 上联系我们的团队。

功能

  • 数据库持久化 - 自动同步客户和订阅数据到您的数据库
  • 访问管理 - 根据用户订阅状态自动授予或撤销访问权限
  • 客户同步 - 将 Creem 客户 ID 与数据库用户同步
  • 结账集成 - 为已认证用户自动或为未认证用户手动创建结账会话
  • 客户门户 - 允许用户管理订阅、查看发票、更新支付方式
  • 订阅管理 - 为已认证用户自动或为未认证用户手动取消、检索和跟踪订阅详情
  • 交易历史 - 为已认证用户自动或为未认证用户手动搜索和过滤交易记录
  • Webhook 处理 - 通过签名验证安全处理 Creem webhook
  • 灵活架构 - 可使用 Better Auth 端点或直接调用服务器端函数
  • 试用滥用防护 - 在数据库模式下,用户整个账户所有计划仅能享受一次试用

安装

安装插件

npm install @creem_io/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 migrate
npx 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,
})

本地开发(可选)

本地测试时,可使用类似 ngrok 的工具将本地服务器暴露到公网:

ngrok http 3000

并将 ngrok 生成的网址添加到 Creem webhook 配置。

数据库模式

启用 persistSubscriptions: true 时,插件创建以下数据表:

Creem 订阅表

表名:creem_subscription

字段类型描述
idstring主键
productIdstringCreem 产品 ID
referenceIdstring您的用户或组织 ID
creemCustomerIdstringCreem 客户 ID
creemSubscriptionIdstringCreem 订阅 ID
creemOrderIdstringCreem 订单 ID
statusstring订阅状态
periodStartdate账单周期起始日期
periodEnddate账单周期结束日期
cancelAtPeriodEndboolean是否订阅将在周期结束时取消

用户表扩展字段

字段类型描述
creemCustomerIdstring关联用户到 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(必需)- Creem 产品 ID
  • units - 订购数量(默认:1)
  • successUrl - 支付成功后跳转地址
  • discountCode - 使用的折扣码
  • customer - 客户信息(自动从会话填充)
  • metadata - 附加元数据(自动包含用户 ID 作为 referenceId
  • requestId - 防止重复请求的幂等性 key

客户门户

重定向用户管理其订阅:

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 处理方案,支持细粒度事件处理及高阶访问控制处理器。

高阶访问控制处理器(推荐)

这些处理器提供最简单且强大的用户访问管理方式。它们自动处理所有支付场景及订阅状态,无需单独管理每个订阅事件。

需要数据库持久化:此类处理器需在插件配置中启用数据库持久化选项。

处理器名称数据参数类型描述
onGrantAccessGrantAccessContext**当应授予用户访问权限时调用。**自动处理支付成功、活动订阅及试用期。可用于启用功能、加入用户组或更新权限。
onRevokeAccessRevokeAccessContext**当应撤销用户访问权限时调用。**自动处理取消、过期、退款及支付失败等情况。可用于禁用功能、移除用户组或撤销权限。

为何选择这些处理器?

  • 访问控制的单一真相来源
  • 自动处理所有支付及订阅场景
  • 降低代码复杂度及潜在错误
  • 适用于一次性购买及订阅
  • 考虑当前账期及访问过期时间
// 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 - 当前订阅周期结束未续订

细粒度事件处理器

高级用例需要对特定事件进行精细控制时,使用以下处理器:

处理器名称数据参数类型描述
onCheckoutCompletedFlatCheckoutCompleted结账成功完成时调用。
onRefundCreatedFlatRefundCreated退款操作发生时触发。
onDisputeCreatedFlatDisputeCreated支付争议或退单创建时触发。
onSubscriptionActiveFlatSubscriptionEvent订阅激活时触发。
onSubscriptionTrialingFlatSubscriptionEvent订阅进入试用期时触发。
onSubscriptionCanceledFlatSubscriptionEvent订阅取消时触发。
onSubscriptionPaidFlatSubscriptionEvent收到订阅付款时触发。
onSubscriptionExpiredFlatSubscriptionEvent订阅到期(无续费/付款)时触发。
onSubscriptionUnpaidFlatSubscriptionEvent订阅付款失败或欠费时触发。
onSubscriptionUpdateFlatSubscriptionEvent订阅设置或详情更新时触发。
onSubscriptionPastDueFlatSubscriptionEvent订阅付款逾期时触发。
onSubscriptionPausedFlatSubscriptionEvent订阅被暂停(用户或管理员)时触发。

如何使用 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} 天`);

数据库模式与 API 模式

插件支持两种运行模式:

数据库模式(推荐)

persistSubscriptions: true(默认)时,订阅数据存储在您的数据库中。

优势:

  • 检查访问权限时无需调用 API,响应更快
  • 离线访问订阅数据
  • 使用 SQL 进行订阅查询
  • 通过 webhook 自动同步数据
  • 防止试用滥用

用法:

creem({
  apiKey: process.env.CREEM_API_KEY!,
  persistSubscriptions: true, // 默认开启
})

API 模式

persistSubscriptions: false,所有数据均直接来自 Creem API。

优势:

  • 无需数据库模式定义
  • 初次设置更简单

限制:

  • 每次访问检查都需 API 调用
  • 部分功能需自行实现
  • 无内置试用滥用防范

用法:

creem({
  apiKey: process.env.CREEM_API_KEY!,
  persistSubscriptions: false,
})

API 模式下,checkSubscriptionAccesshasAccessGranted 等函数功能受限,可能需直接使用 Creem SDK 实现自定义逻辑。

类型导出说明

服务端类型

类型名称描述典型用法
CreemOptionsCreem 插件的配置选项,如 API Key 和持久化设置。用于服务器端插件配置。
GrantAccessContext授予用户访问权限时传递给自定义访问钩子的上下文。用于自定义访问逻辑。
RevokeAccessContext撤销用户访问权限时钩子传递的上下文。用于自定义访问逻辑。
GrantAccessReason表示授予访问权限原因的枚举或类型(例如支付成功、试用激活)。访问相关钩子和事件返回。
RevokeAccessReason表示撤销访问权限原因的枚举或类型(例如取消、支付失败)。访问相关钩子和事件返回。
FlatCheckoutCompleted结账完成 webhook 负载的事件对象类型。用于 webhook 处理器和事件监听。
FlatRefundCreated退款创建 webhook 负载事件对象类型。用于 webhook 处理器和事件监听。
FlatDisputeCreated支付争议创建 webhook 负载事件对象类型。用于 webhook 处理器和事件监听。
FlatSubscriptionEvent通用订阅事件(创建、更新、取消等)的事件对象类型。用于 webhook 处理器和事件监听。

客户端类型

类型名称描述
CreateCheckoutInput创建结账会话时的输入参数类型。
CreateCheckoutResponse创建结账会话请求的响应数据类型。
CheckoutCustomer结账会话中客户信息类型。
CreatePortalInput创建客户门户会话时的输入参数类型。
CreatePortalResponse创建客户门户请求的响应数据类型。
CancelSubscriptionInput取消订阅时的输入参数类型。
CancelSubscriptionResponse取消订阅请求的响应数据类型。
RetrieveSubscriptionInput获取特定订阅详情的输入类型。
SubscriptionDataAPI 返回的订阅信息类型。
SearchTransactionsInput搜索交易时的筛选条件及参数类型。
SearchTransactionsResponse交易搜索查询的响应结构类型。
TransactionData单笔交易(支付、退款等)相关数据类型。
HasAccessGrantedResponse指示用户基于订阅状态/规则是否拥有访问权限的响应数据结构。

试用滥用防护

启用数据库模式(persistSubscriptions: true)时,插件将自动防止试用滥用。用户整个账户内所有订阅计划仅能获得一次试用机会。

示例场景:

  1. 用户订阅 “Starter” 计划并享受 7 天试用
  2. 试用期间用户取消订阅
  3. 用户尝试订阅 “Premium” 计划
  4. 不再提供试用,立即收费

该防护为自动且不可配置,试用资格在创建订阅时判断,无法覆盖。

常见问题排查

Webhook 问题

Webhook 无法正确处理时:

  1. 确认 Creem 控制面板中 webhook URL 是否正确
  2. 检查 webhook 签名密钥是否匹配
  3. 确保在 Creem 控制面板中选中所有必要事件
  4. 查看服务器日志中 webhook 处理错误
  5. 用 Creem 的 webhook 测试工具测试发送

订阅状态异常

订阅状态未更新时:

  1. 确认 webhook 是否有正确接收和处理
  2. 确认 creemCustomerIdcreemSubscriptionId 字段已填充
  3. 检查您应用和 Creem 之间的引用 ID 是否一致
  4. 查看 webhook 处理器日志中是否有错误

数据库模式无效

数据库持久化不生效时:

  1. 确认启用了 persistSubscriptions: true(默认开启)
  2. 运行迁移命令:npx auth migrate
  3. 检查数据库连接是否正常
  4. 确认数据库表模式是否成功创建
  5. 检查数据库适配器配置

API 模式限制

部分功能仅数据库模式支持或者需额外参数:

  • checkSubscriptionAccess 需传入 userId
  • getActiveSubscriptions 需传入 userId
  • 无自动试用滥用防护
  • 无客户端 hasAccessGranted 方法

使用时建议启用数据库模式,或自行用 Creem SDK 实现自定义逻辑。

额外资源

支持

遇到问题或有疑问: