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 (required) - The Creem product ID
  • units - Number of units (default: 1)
  • successUrl - Redirect URL after successful payment
  • discountCode - Discount code to apply
  • customer - Customer information (auto-populated from session)
  • metadata - Additional metadata (auto-includes user ID as referenceId)
  • 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.

处理器名称数据参数类型描述
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} 天`);

数据库模式 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 模式下,checkSubscriptionAccesshasAccessGranted 等函数功能受限,可能需直接使用 Creem SDK 实现自定义逻辑。

类型导出说明

服务端类型

Type NameDescriptionTypical Usage
CreemOptionsConfiguration options for the Creem plugin, such as API keys and persistence settings.Used to configure the plugin on the server.
GrantAccessContextContext passed to custom access control hooks when granting access to a user.Used in custom access logic.
RevokeAccessContextContext passed to hooks when revoking user access due to subscription status changes.Used in custom access logic.
GrantAccessReasonEnum or type describing reasons for granting access (e.g., payment received, trial activated).Returned in access-related hooks and events.
RevokeAccessReasonEnum or type describing reasons for revoking access (e.g., canceled, payment failed).Returned in access-related hooks and events.
FlatCheckoutCompletedEvent object type for webhook payload when a checkout completes successfully.Used in webhook handlers and event listeners.
FlatRefundCreatedEvent object type for webhook payload when a refund is created.Used in webhook handlers and event listeners.
FlatDisputeCreatedEvent object type for webhook payload when a dispute is created.Used in webhook handlers and event listeners.
FlatSubscriptionEventEvent object type for generic subscription events (created, updated, canceled, etc).Used in webhook handlers and event listeners.

客户端类型

Type NameDescription
CreateCheckoutInputInput parameters for creating a checkout session.
CreateCheckoutResponseResponse shape for a checkout session creation request.
CheckoutCustomerCustomer information type used in a checkout session.
CreatePortalInputInput parameters for creating a customer portal session.
CreatePortalResponseResponse data for a request to create a customer portal.
CancelSubscriptionInputInput parameters when cancelling a subscription.
CancelSubscriptionResponseResponse data for a subscription cancellation request.
RetrieveSubscriptionInputInput for retrieving a specific subscription's details.
SubscriptionDataSubscription information structure as returned by the API.
SearchTransactionsInputFilters and parameters for searching transactions.
SearchTransactionsResponseResponse structure for a transaction search query.
TransactionDataData relating to individual transactions (e.g., payment, refund, etc).
HasAccessGrantedResponseThe shape of the response indicating whether a user has access based on subscription status/rules.

试用滥用防护

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

Example Scenario:

  1. User subscribes to "Starter" plan with 7-day trial
  2. User cancels subscription during the trial period
  3. User attempts to subscribe to "Premium" plan
  4. No trial is offered - user is charged immediately

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

常见问题排查

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 requires passing the userId parameter
  • getActiveSubscriptions requires passing the userId parameter
  • No automatic trial abuse prevention
  • No access to hasAccessGranted client method

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

额外资源

支持

遇到问题或有疑问: