Creem

Creem 是一个金融操作系统,使全球销售软件的团队和个人能够拆分收入并协作处理财务工作流程,而无需担心任何税务合规问题。这个插件将 Creem 与 Better Auth 集成,将支付处理和订阅管理直接引入你的身份验证层。

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

Get support on Creem Discord or in our in-app live-chat

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

特性

【Features】

  • 数据库持久化 - 自动将客户和订阅数据与你的数据库同步
  • 访问管理 - 根据用户的订阅状态自动授予或撤销访问权限
  • 客户同步 - 将 Creem 客户 ID 与你的数据库用户同步
  • 结账集成 - 为已认证用户自动创建结账会话,或为未认证用户手动创建结账会话
  • 客户门户 - 让用户管理订阅、查看发票并更新支付方式
  • 订阅管理 - 为已验证用户取消、检索和跟踪订阅详情,或为未验证用户手动操作
  • 交易历史 - 为已认证用户搜索和筛选交易记录,或为未认证用户手动操作
  • Webhook 处理 - 通过签名验证安全处理 Creem webhook
  • 灵活的架构 - 使用更好的身份验证端点或直接的服务器端功能
  • 试用滥用防范 - 用户在所有计划中每个账户只能获得一次试用(使用数据库模式时)

安装

【Installation】

安装插件

npm install @creem_io/better-auth

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

获取你的 API 密钥

Creem 控制台 的“开发者”菜单中获取你的 Creem API 密钥,并将其添加到你的环境变量中:

# .env
CREEM_API_KEY=your_api_key_here

测试模式和生产模式使用不同的 API 密钥。请确保你使用的是适合你环境的正确密钥。

配置

【Configuration】

服务器配置

【Server Configuration】

使用 Creem 插件配置更好的身份验证:

【Configure Better Auth with the Creem plugin:】

// lib/auth.ts
import { betterAuth } from "better-auth";
import { creem } from "@creem_io/better-auth";

export const auth = betterAuth({
  database: {
    // your database config
  },
  plugins: [
    creem({
      apiKey: process.env.CREEM_API_KEY!,
      webhookSecret: process.env.CREEM_WEBHOOK_SECRET, // Optional, webhooks are automatically enabled when passing a signing secret
      testMode: true, // Optional, use test mode for development
      defaultSuccessUrl: "/success", // Optional, redirect to this URL after successful payments
      persistSubscriptions: true, // Optional, enable database persistence (default: true)
    }),
  ],
});

客户端配置

【Client Configuration】

标准设置

【Standard Setup】

// 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)

【Enhanced TypeScript Support (React-Only)】

为了提高 TypeScript 的智能感知和自动补全:

【For improved TypeScript IntelliSense and autocomplete:】

// 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 插件的使用进行了优化。

数据库迁移

【Database Migration】

如果你使用数据库持久化(persistSubscriptions: true),请生成并运行数据库架构:

【If you're using database persistence (persistSubscriptions: true), generate and run the database schema:】

npx @better-auth/cli migrate
npx @better-auth/cli generate

根据你的数据库适配器,可能需要额外的设置步骤。详情请参考 Better Auth 适配器文档

Webhook 设置

【Webhook Setup】

创建 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 URL 添加到你的 Creem webhook 设置中。

数据库架构

【Database Schema】

persistSubscriptions: true 时,插件会创建以下模式:

【When persistSubscriptions: true, the plugin creates the following schema:】

Creem 订阅表

【Creem Subscription Table】

表名:creem_subscription

【Table Name: creem_subscription

字段类型描述
id字符串主键
productId字符串Creem 产品 ID
referenceId字符串你的用户/组织 ID
creemCustomerId字符串Creem 客户 ID
creemSubscriptionId字符串Creem 订阅 ID
creemOrderId字符串Creem 订单 ID
status字符串订阅状态
periodStart日期计费周期开始日期
periodEnd日期计费周期结束日期
cancelAtPeriodEnd布尔值是否在周期结束时取消订阅

用户表扩展

【User Table Extension】

字段类型描述
creemCustomerId字符串将用户与 Creem 客户关联

用法

【Usage】

结账

【Checkout】

创建结账会话以处理付款:

【Create a checkout session to process payments:】

"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", // Optional
      metadata: { planType: "pro" }, // Optional
    });

    if (data?.url) {
      window.location.href = data.url;
    }
  };

  return <button onClick={handleCheckout}>Subscribe Now</button>;
}

结账选项

【Checkout Options】

  • productId(必填)- Creem 产品 ID
  • units - 单元数量(默认值:1)
  • successUrl - 支付成功后的重定向 URL
  • discountCode - 要使用的折扣码
  • customer - 客户信息(从会话自动填充)
  • metadata - 额外的元数据(自动包含用户ID作为 referenceId
  • requestId - 用于防止重复的幂等键

客户门户

【Customer Portal】

将用户引导至管理他们的订阅:

【Redirect users to manage their subscriptions:】

const handlePortal = async () => {
  // No need to redirect, the portal will be opened in the same tab
  const { data, error } = await authClient.creem.createPortal();
};

订阅管理

【Subscription Management】

取消订阅

【Cancel Subscription】

当启用数据库持久化时,订阅会自动为已认证的用户找到:

【When database persistence is enabled, the subscription is found automatically for the authenticated user:】

const handleCancel = async () => {
  const { data, error } = await authClient.creem.cancelSubscription();

  if (data?.success) {
    console.log(data.message);
  }
};

如果禁用数据库持久化,请提供订阅 ID:

【If database persistence is disabled, provide the subscription ID:】

const { data } = await authClient.creem.cancelSubscription({
  id: "sub_123456",
});

检索订阅

【Retrieve Subscription】

获取已认证用户的订阅详情:

【Get subscription details for the authenticated user:】

const getSubscription = async () => {
  const { data } = await authClient.creem.retrieveSubscription();

  if (data) {
    console.log(`Status: ${data.status}`);
    console.log(`Product: ${data.product.name}`);
    console.log(`Price: ${data.product.price} ${data.product.currency}`);
  }
};

检查访问权限

【Check Access】

验证用户是否有有效订阅(需要数据库模式):

【Verify if the user has an active subscription (requires database mode):】

const { data } = await authClient.creem.hasAccessGranted();

if (data?.hasAccess) {
  // User has active subscription access
  console.log(`Expires: ${data.expiresAt}`);
}

此功能会检查用户在当前计费周期是否有访问权限。例如,如果用户购买了一年的套餐,但在一个月后取消,他们仍然可以访问直到年度结束。

交易记录

【Transaction History】

搜索已认证用户的交易记录:

【Search transaction records for the authenticated user:】

const { data } = await authClient.creem.searchTransactions({
  productId: "prod_xyz789", // Optional filter
  pageNumber: 1,
  pageSize: 50,
});

if (data?.transactions) {
  data.transactions.forEach((tx) => {
    console.log(`${tx.type}: ${tx.amount} ${tx.currency}`);
  });
}

Webhook 处理

【Webhook Handling】

该插件提供灵活的 Webhook 处理功能,同时支持细粒度的事件处理程序和高级访问控制处理程序。

【The plugin provides flexible webhook handling with both granular event handlers and high-level access control handlers.】

【High-Level Access Control Handlers (Recommended)】

这些处理程序提供了管理用户访问的最简单且最强大的方式。它们会自动处理所有支付场景和订阅状态,因此你无需管理单独的订阅事件。

【These handlers provide the simplest and most powerful way to manage user access. They automatically handle all payment scenarios and subscription states, so you don't need to manage individual subscription events.】

需要数据库持久化: 这些处理程序需要在你的插件配置中启用数据库持久化选项。

处理程序名称数据参数类型描述
onGrantAccessGrantAccessContext当应授予用户访问权限时调用。 处理成功付款、活跃订阅和试用期。可用于启用功能、将用户添加到组或更新权限。
onRevokeAccessRevokeAccessContext当应撤销用户访问权限时调用。 处理取消、过期、退款和付款失败。可用于禁用功能、从组中移除或撤销权限。

为什么使用这些处理程序?

  • 访问控制的唯一可信来源
  • 自动处理所有支付场景
  • 减少代码复杂性和潜在错误
  • 适用于一次性购买和订阅
  • 考虑当前计费周期和访问到期日期
// lib/auth.ts
import { betterAuth } from "better-auth";
import { creem } from "@creem_io/better-auth";

export const auth = betterAuth({
  database: {
    // your database config
  },
  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;

        // Update your database specific logic
        await db.user.update({
          where: { id: userId },
          data: { 
            hasAccess: true, 
            subscriptionTier: product.name,
            accessReason: reason 
          },
        });

        console.log(`Granted ${reason} access to ${customer.email}`);
      },

      onRevokeAccess: async ({ reason, product, customer, metadata }) => {
        const userId = metadata?.referenceId as string;

        // Update your database specific logic
        await db.user.update({
          where: { id: userId },
          data: { 
            hasAccess: false, 
            revokeReason: reason 
          },
        });

        console.log(`Revoked access (${reason}) from ${customer.email}`);
      },
    }),
  ],
})

授权访问原因

【Grant Access Reasons】

  • subscription_active - 订阅已激活
  • subscription_trialing - 订阅处于试用期
  • subscription_paid - 已收到订阅付款

撤销访问原因

【Revoke Access Reasons】

  • subscription_paused - 订阅已被用户或管理员暂停
  • subscription_expired - 订阅已过期未续订
  • subscription_period_end - 当前订阅周期已结束,未续订

细粒度事件处理器

【Granular Event Handlers】

对于需要对特定事件进行精细控制的高级用例,请使用这些处理程序:

【For advanced use cases where you need fine-grained control over specific events, use these handlers:】

Handler NameData Parameter TypeDescription
onCheckoutCompletedFlatCheckoutCompletedCalled when a checkout is completed successfully.
onRefundCreatedFlatRefundCreatedTriggered when a refund is issued for a payment.
onDisputeCreatedFlatDisputeCreatedInvoked when a payment dispute/chargeback is created.
onSubscriptionActiveFlatSubscriptionEventFired when a subscription becomes active.
onSubscriptionTrialingFlatSubscriptionEventSubscription enters a trial period.
onSubscriptionCanceledFlatSubscriptionEventCalled when a subscription is canceled.
onSubscriptionPaidFlatSubscriptionEventSubscription payment is received.
onSubscriptionExpiredFlatSubscriptionEventSubscription has expired (no renewal/payment).
onSubscriptionUnpaidFlatSubscriptionEventPayment for a subscription failed or remains unpaid.
onSubscriptionUpdateFlatSubscriptionEventSubscription settings/details updated.
onSubscriptionPastDueFlatSubscriptionEventSubscription payment is late or overdue.
onSubscriptionPausedFlatSubscriptionEventSubscription has been paused (by user or admin).

如何使用 Webhook 处理器

【How to use a Webhook Handler】

处理单个 webhook 事件,所有属性已扁平化以便轻松访问:

【Handle individual webhook events with all properties flattened for easy access:】

// lib/auth.ts
import { betterAuth } from "better-auth";
import { creem } from "@creem_io/better-auth";

export const auth = betterAuth({
  database: {
    // your database config
  },
  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} purchased ${product.name}`);
        
        // Perfect for one-time payments
        await sendThankYouEmail(customer.email);
      },

      onSubscriptionActive: async (data) => {
        const { customer, product, status } = data;
        // Handle active subscription
      },

      onSubscriptionTrialing: async (data) => {
        // Handle trial period
      },

      onSubscriptionCanceled: async (data) => {
        // Handle cancellation
      },

      onSubscriptionExpired: async (data) => {
        // Handle expiration
      },

      onRefundCreated: async (data) => {
        // Handle refunds
      },

      onDisputeCreated: async (data) => {
        // Handle disputes
      },
    }),
  ],
});

自定义 Webhook 处理器

【Custom Webhook Handler】

创建你自己的带签名验证的 webhook 端点:

【Create your own webhook endpoint with signature verification:】

// 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("Invalid signature", { status: 401 });
  }

  const event = JSON.parse(payload);
  // Your custom webhook handling logic

  return Response.json({ received: true });
}

服务器端功能

【Server-Side Functions】

可以直接在服务器组件、服务器操作或 API 路由中使用这些实用工具,无需通过 Better Auth 端点。

【Use these utilities directly in Server Components, Server Actions, or API routes without going through Better Auth endpoints.】

导入服务器工具

【Import Server Utilities】

import {
  createCheckout,
  createPortal,
  cancelSubscription,
  retrieveSubscription,
  searchTransactions,
  checkSubscriptionAccess,
  isActiveSubscription,
  formatCreemDate,
  getDaysUntilRenewal,
  validateWebhookSignature,
} from "@creem_io/better-auth/server";

服务器组件示例

【Server Component Example】

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>Welcome to Dashboard</h1>
      <p>Subscription Status: {status.status}</p>
      {status.expiresAt && (
        <p>Renews: {status.expiresAt.toLocaleDateString()}</p>
      )}
    </div>
  );
}

服务器操作示例

【Server Action Example】

"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("Not authenticated");
  }

  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);
}

中间件示例

【Middleware Example】

根据订阅状态保护路由:

【Protect routes based on subscription status:】

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*"],
};

工具函数

【Utility Functions】

import {
  isActiveSubscription,
  formatCreemDate,
  getDaysUntilRenewal,
} from "@creem_io/better-auth/server";

// Check if status grants access
if (isActiveSubscription(subscription.status)) {
  // User has access
}

// Format Creem timestamps
const renewalDate = formatCreemDate(subscription.next_billing_date);
console.log(renewalDate.toLocaleDateString());

// Calculate days until renewal
const days = getDaysUntilRenewal(subscription.current_period_end_date);
console.log(`Renews in ${days} days`);

数据库模式 vs API 模式

【Database Mode vs API Mode】

该插件支持两种操作模式:

【The plugin supports two operational modes:】

【Database Mode (Recommended)】

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

【When persistSubscriptions: true (default), subscription data is stored in your database.】

好处:

  • 无需 API 调用即可快速访问检查
  • 离线访问订阅数据
  • 使用 SQL 查询订阅
  • 通过 Webhook 自动同步
  • 防止试用滥用

用法:

creem({
  apiKey: process.env.CREEM_API_KEY!,
  persistSubscriptions: true, // Default
})

API 模式

【API Mode】

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

【When persistSubscriptions: false, all data comes directly from the Creem API.】

优点:

  • 不需要数据库模式
  • 初始设置更简单

限制:

  • 每次访问检查都需要调用 API
  • 某些功能需要自定义实现
  • 没有内置的试用防滥用功能

用法:

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

在 API 模式下,像 checkSubscriptionAccesshasAccessGranted 这样的功能功能有限,可能需要使用 Creem SDK 直接进行自定义实现。

类型导出

【Type Exports】

服务器端类型

【Server-Side Types】

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.

客户端类型

【Client-Side Types】

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.

试用滥用防护

【Trial Abuse Prevention】

在使用数据库模式(persistSubscriptions: true)时,插件会自动防止试用滥用。用户在所有订阅计划中只能获得一次试用。

【When using database mode (persistSubscriptions: true), the plugin automatically prevents trial abuse. Users can only receive one trial across all subscription plans.】

示例情景:

  1. 用户订阅“入门”计划并享有 7 天试用期
  2. 用户在试用期内取消订阅
  3. 用户尝试订阅“高级”计划
  4. 不提供试用 - 用户将立即被收费

此保护是自动的,无需配置。试用资格在订阅创建时确定,无法更改。

【This protection is automatic and requires no configuration. Trial eligibility is determined when the subscription is created and cannot be overridden.】

故障排除

【Troubleshooting】

Webhook 问题

【Webhook Issues】

如果网络钩子处理不正确:

【If webhooks aren't being processed correctly:】

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

订阅状态问题

【Subscription Status Issues】

如果订阅状态没有更新:

【If subscription statuses aren't updating:】

  1. 确认正在接收和处理 Webhook
  2. 验证 creemCustomerIdcreemSubscriptionId 字段是否已填充
  3. 检查你的应用和Creem之间的参考ID是否匹配
  4. 查看 webhook 处理程序日志中的错误

数据库模式无法使用

【Database Mode Not Working】

如果数据库持久化功能无法正常工作:

【If database persistence isn't functioning:】

  1. 确保设置了 persistSubscriptions: true(这是默认值)
  2. 运行迁移:npx @better-auth/cli migrate
  3. 验证数据库连接是否正常
  4. 检查模式表是否创建成功
  5. 审查数据库适配器配置

API 模式的限制

【API Mode Limitations】

某些功能仅在数据库模式下可用,或需要传递额外的参数:

【Some functionalities are only available in database mode or require extra parameters to be passed:】

  • checkSubscriptionAccess 需要传入 userId 参数
  • getActiveSubscriptions 需要传入 userId 参数
  • 没有自动试用防滥用措施
  • 无法访问 hasAccessGranted 客户端方法

要使用这些功能,你可以启用数据库模式,或直接使用 Creem SDK 实现自定义逻辑。

【To use these features, either enable database mode or implement custom logic using the Creem SDK directly.】

附加资源

【Additional Resources】

支持

【Support】

如有问题或疑问:

【For issues or questions:】