# Creem (/docs/plugins/creem)

用于 Creem 结账、订阅和访问控制的 Better Auth 插件。



[Creem](https://creem.io) 是面向全球销售软件的开发者的记录商家（Merchant of Record）。支付、订阅、税务合规、拒付、收入分成、付款。全部由 Creem 处理。您只需专注于自己的产品。

`@creem_io/better-auth` 插件将 Creem 与您的 Better Auth 用户连接起来。它添加了与会话关联的结账、客户门户、订阅和交易历史记录端点。经过验证的 webhook 会将订阅状态保存在您的数据库中，并调用您自己的访问权限回调。

<Callout>
  此插件由 Creem 团队维护。如需报告错误、问题或功能请求，
  请访问 [Creem GitHub 仓库](https://github.com/armitage-labs/creem)。
  完整参考文档请参阅 [Creem 文档](https://docs.creem.io/code/sdks/better-auth)。
</Callout>

<Callout type="warn">
  2.0 版本更改了结账和门户重定向、错误响应以及取消后的访问权限。如果您从 1.x 升级，请按照 [2.0 迁移指南](https://docs.creem.io/code/sdks/better-auth/migration#upgrading-from-1-x)操作。
</Callout>

<Card href="https://discord.gg/q3GKZs92Av" title="在 Creem Discord 或应用内实时聊天中获得支持">
  需要帮助？随时在 Discord 上联系我们的团队。
</Card>

## 功能特性 [#功能特性]

* 为已登录用户或匿名访客创建结账
* 打开 Creem 客户门户，以管理发票、付款方式和订阅
* 取消和检索订阅，以及搜索交易
* 将经过验证的 webhook 中的客户和订阅信息持久化
* 根据本地订阅状态检查访问权限，包括取消后已付款的期限
* 使用 `onGrantAccess` 和 `onRevokeAccess` 运行您自己的权限配置逻辑
* 为每位用户提供至多一次试用机会，适用于所有套餐

## 安装 [#安装]

<Steps>
  <Step>
    ### 安装插件 [#安装插件]

    <CodeBlockTabs defaultValue="npm" groupId="persist-install">
      <CodeBlockTabsList>
        <CodeBlockTabsTrigger value="npm">
          npm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="pnpm">
          pnpm
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="yarn">
          yarn
        </CodeBlockTabsTrigger>

        <CodeBlockTabsTrigger value="bun">
          bun
        </CodeBlockTabsTrigger>
      </CodeBlockTabsList>

      <CodeBlockTab value="npm">
        ```bash
        npm install @creem_io/better-auth
        ```
      </CodeBlockTab>

      <CodeBlockTab value="pnpm">
        ```bash
        pnpm add @creem_io/better-auth
        ```
      </CodeBlockTab>

      <CodeBlockTab value="yarn">
        ```bash
        yarn add @creem_io/better-auth
        ```
      </CodeBlockTab>

      <CodeBlockTab value="bun">
        ```bash
        bun add @creem_io/better-auth
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    此插件需要 `better-auth` 1.5.6 或更高版本，并包含 Creem TypeScript SDK。

    <Callout>
      如果您采用前后端分离的架构，请确保在项目的客户端和服务端部分都安装此插件。
    </Callout>
  </Step>

  <Step>
    ### 添加环境变量 [#添加环境变量]

    从 [Creem 控制面板](https://creem.io/dashboard/developers)获取您的 API 密钥。您可以在[配置 webhook 步骤](#set-up-webhooks)中获取 webhook 密钥。

    ```txt title=".env"
    CREEM_API_KEY=your_api_key
    CREEM_WEBHOOK_SECRET=your_webhook_secret
    ```

    <Callout type="warn">
      测试模式和生产环境使用不同的 API 密钥和 webhook 密钥。`testMode` 默认为 `false`，因此每当您使用测试模式密钥时，都应将其设为 `true`。
    </Callout>
  </Step>

  <Step>
    ### 将插件添加到 auth 配置中 [#将插件添加到-auth-配置中]

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

    export const auth = betterAuth({
        // ... your existing config
        plugins: [
            creem({
                apiKey: process.env.CREEM_API_KEY!,
                webhookSecret: process.env.CREEM_WEBHOOK_SECRET!,
                testMode: process.env.NODE_ENV !== "production",
                defaultSuccessUrl: "/billing/success",
            }),
        ],
    });
    ```
  </Step>

  <Step>
    ### 添加客户端插件 [#添加客户端插件]

    ```ts title="auth-client.ts"
    import { createAuthClient } from "better-auth/react";
    import { creemClient } from "@creem_io/better-auth/client";

    export const authClient = createAuthClient({
        plugins: [creemClient()],
    });
    ```

    标准客户端会推断端点的输入和响应，以及会话字段 `creemCustomerId` 和 `hadTrial`。1.x 中的 `createCreemAuthClient` 包装器已弃用。
  </Step>

  <Step>
    ### 迁移数据库 [#迁移数据库]

    运行迁移或生成架构，以便在数据库中添加必要的表。

    <Tabs items="[&#x22;migrate&#x22;, &#x22;generate&#x22;]">
      <Tab value="migrate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install">
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx auth@latest migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx auth@latest migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx auth@latest migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x auth@latest migrate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>

      <Tab value="generate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install">
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx auth@latest generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx auth@latest generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx auth@latest generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x auth@latest generate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>
    </Tabs>

    `migrate` 使用 Better Auth 内置的 Kysely 适配器应用架构。如果由 Prisma、Drizzle 或其他 ORM 管理架构，则 `generate` 会改为写入架构，然后由您使用 ORM 的迁移工具应用。

    请参阅[架构](#schema)部分，了解插件添加的表和字段。如果您设置了 `persistSubscriptions: false`，则可跳过此步骤。
  </Step>

  <Step>
    ### 配置 webhook [#配置-webhook]

    在 [Creem 控制面板](https://creem.io/dashboard/developers/webhooks)中创建 webhook 端点，并将其指向：

    ```txt
    https://your-domain.com/api/auth/creem/webhook
    ```

    `/api/auth` 是 auth 服务器的默认路径。将签名密钥复制到 `CREEM_WEBHOOK_SECRET`。只有设置了 `webhookSecret`，插件才会注册 webhook 路由。

    在本地开发时，可使用 [ngrok](https://ngrok.com) 等隧道工具公开您的服务器（`ngrok http 3000`），并改为注册隧道 URL。
  </Step>
</Steps>

## 使用方法 [#使用方法]

每个客户端方法都会返回 Better Auth 的 `{ data, error }` 结果。请求失败时，`data` 为 `null`，而 `error` 包含 `message` 和 `status`。在服务器上通过 `auth.api` 发起的调用则会抛出 `APIError`。

### 结账 [#结账]

```ts
const { data, error } = await authClient.creem.createCheckout({
    productId: "prod_abc123",
    successUrl: "/billing/success",
});

if (!error && data.url) {
    window.location.assign(data.url);
}
```

结账会返回 URL，由您负责跳转。传入 `redirect: true` 可让 Better Auth 自动重定向。

| 选项             | 描述                                           |
| -------------- | -------------------------------------------- |
| `productId`    | Creem 产品 ID。&#x2A;*必填。**                     |
| `units`        | 单位数量。默认值：`1`。                                |
| `successUrl`   | Creem 在付款后将客户发送到的位置。会覆盖 `defaultSuccessUrl`。 |
| `discountCode` | 要使用的 Creem 折扣码。                              |
| `customer`     | `{ email }`。默认为已登录用户的电子邮件地址。                 |
| `customFields` | 最多三个结账时收集的文本或复选框字段。                          |
| `metadata`     | 您自己的元数据。插件会将已登录用户的 ID 添加为 `referenceId`。     |
| `requestId`    | 幂等键，确保重试请求时不会创建第二笔结账。                        |
| `redirect`     | 自动重定向到结账 URL。默认值：`false`。                    |

webhook 通过 `metadata.referenceId` 将购买记录关联到用户。未通过会话创建的结账没有 `referenceId`，因此插件无法为任何用户保存其客户或订阅信息。

### 客户门户 [#客户门户]

门户允许客户管理订阅、查看发票和更新付款方式。

```ts
const { data, error } = await authClient.creem.createPortal();

if (!error && data.url) {
    window.location.assign(data.url);
}
```

与结账一样，门户的 `redirect` 默认值为 `false`。该端点使用已登录用户存储的 `creemCustomerId`。它也接受 `customerId`，但不会检查该 ID 是否属于当前用户，因此绝不要传入来自不可信输入的 ID。

### 检查访问权限 [#检查访问权限]

`hasAccessGranted` 会读取已登录用户存储的订阅，因此需要启用持久化。

```ts
const { data, error } = await authClient.creem.hasAccessGranted();

if (!error && data.hasAccessGranted) {
    console.log(`Access until ${data.subscription?.periodEnd}`);
}
```

订阅状态为 `active`、`trialing` 或 `paid` 时，用户可以访问。状态为 `scheduled_cancel`、`past_due` 或 `unpaid` 的订阅会保留访问权限，直到 `periodEnd`，因此客户可以使用已付费的剩余时间。状态为 `canceled` 的订阅不授予访问权限。

未有用户登录时，请求会返回状态码 `401`；禁用持久化时，请求会返回状态码 `400`。只要设置了 `error`，就应拒绝访问。

在服务器上，使用请求标头调用同一端点。用户退出登录时，`auth.api.hasAccessGranted` 会抛出 `APIError`，因此请先检查会话：

```tsx title="app/dashboard/page.tsx"
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

export default async function DashboardPage() {
    const requestHeaders = await headers();
    const session = await auth.api.getSession({ headers: requestHeaders });

    if (!session) {
        redirect("/sign-in");
    }

    const access = await auth.api.hasAccessGranted({
        headers: requestHeaders,
    });

    if (!access.hasAccessGranted) {
        redirect("/pricing");
    }

    return <h1>Dashboard</h1>;
}
```

### 取消订阅 [#取消订阅]

```ts
const { data, error } = await authClient.creem.cancelSubscription();

if (!error) {
    console.log(data.message);
}
```

启用持久化后，该端点会取消用户已存储且状态为 `active`、`trialing`、`unpaid` 或 `past_due` 的订阅。只有当用户没有此类订阅时，它才会使用您传入的 `id`。禁用持久化后，必须传入 `id`，且该端点不会检查它是否属于已登录用户。

### 检索订阅 [#检索订阅]

```ts
const { data, error } = await authClient.creem.retrieveSubscription();

if (!error && typeof data.product !== "string") {
    console.log(`${data.status}: ${data.product.name}`);
}
```

启用持久化后，该端点会返回为用户存储的第一条订阅。禁用持久化后，请传入 `id`。`product` 和 `customer` 可以是 ID 或展开的对象，因此读取其属性前请先检查类型。

### 交易历史记录 [#交易历史记录]

```ts
const { data, error } = await authClient.creem.searchTransactions({
    productId: "prod_abc123",
    pageNumber: 1,
    pageSize: 20,
});

for (const transaction of data?.items ?? []) {
    console.log(transaction.type, transaction.amount, transaction.currency);
}
```

响应包含 `items` 和 `pagination`。金额以分为单位。该端点默认使用已登录用户的 `creemCustomerId` 进行搜索，您也可以按 `productId` 或 `orderId` 筛选。

## Webhook [#webhook]

webhook 路由会根据 `webhookSecret` 验证 `creem-signature` 标头，并拒绝不匹配的请求。启用持久化后，它会先更新用户的 `creemCustomerId` 和 `creem_subscription` 表，然后再调用您的回调。

插件会等待每个回调执行完毕后才响应。Creem 可能会多次发送同一事件，因此请确保回调可以安全地重复运行。

### 授予和撤销访问权限 [#授予和撤销访问权限]

`onGrantAccess` 和 `onRevokeAccess` 可让您在同一处配置和移除自有系统中的访问权限：

```ts title="auth.ts"
creem({
    apiKey: process.env.CREEM_API_KEY!,
    webhookSecret: process.env.CREEM_WEBHOOK_SECRET!,
    onGrantAccess: async ({ reason, product, metadata }) => {
        const userId = metadata?.referenceId;
        if (typeof userId !== "string") return;

        await setPlanAccess({ userId, productId: product.id, enabled: true, reason });
    },
    onRevokeAccess: async ({ reason, product, metadata }) => {
        const userId = metadata?.referenceId;
        if (typeof userId !== "string") return;

        await setPlanAccess({ userId, productId: product.id, enabled: false, reason });
    },
});
```

| 回调               | 原因                      | Webhook 事件              |
| ---------------- | ----------------------- | ----------------------- |
| `onGrantAccess`  | `subscription_active`   | `subscription.active`   |
| `onGrantAccess`  | `subscription_trialing` | `subscription.trialing` |
| `onGrantAccess`  | `subscription_paid`     | `subscription.paid`     |
| `onRevokeAccess` | `subscription_paused`   | `subscription.paused`   |
| `onRevokeAccess` | `subscription_expired`  | `subscription.expired`  |
| `onRevokeAccess` | `subscription_canceled` | `subscription.canceled` |

其他事件不会调用这两个回调：

* `subscription.scheduled_cancel` 会保留访问权限，直到当前周期结束。插件会将状态存储为 `scheduled_cancel`，并将 `cancelAtPeriodEnd: true`，而 `hasAccessGranted` 会在 `periodEnd` 过后拒绝访问。如果您在插件表以外跟踪访问权限，请存储周期结束时间并自行执行相关规则。
* 退款、争议和付款失败不会撤销访问权限。请使用下方的事件回调处理这些情况。
* 一次性购买不会创建订阅事件。请在 `onCheckoutCompleted` 中授予相应访问权限。

### 事件回调 [#事件回调]

每个事件也都有自己的回调。回调会接收扁平化的事件数据（可用于解构），并将 Better Auth 端点上下文作为第二个参数。

| 回调                              | 事件                              |
| ------------------------------- | ------------------------------- |
| `onCheckoutCompleted`           | `checkout.completed`            |
| `onRefundCreated`               | `refund.created`                |
| `onDisputeCreated`              | `dispute.created`               |
| `onSubscriptionActive`          | `subscription.active`           |
| `onSubscriptionTrialing`        | `subscription.trialing`         |
| `onSubscriptionPaid`            | `subscription.paid`             |
| `onSubscriptionCanceled`        | `subscription.canceled`         |
| `onSubscriptionScheduledCancel` | `subscription.scheduled_cancel` |
| `onSubscriptionExpired`         | `subscription.expired`          |
| `onSubscriptionUnpaid`          | `subscription.unpaid`           |
| `onSubscriptionPastDue`         | `subscription.past_due`         |
| `onSubscriptionPaused`          | `subscription.paused`           |
| `onSubscriptionUpdate`          | `subscription.update`           |

```ts title="auth.ts"
creem({
    apiKey: process.env.CREEM_API_KEY!,
    webhookSecret: process.env.CREEM_WEBHOOK_SECRET!,
    onCheckoutCompleted: async ({ product, order, metadata }, ctx) => {
        // One-time purchases: grant access here
    },
    onRefundCreated: async ({ refund_amount, order }, ctx) => {
        // Decide whether this refund removes access
    },
});
```

### 自定义 webhook 路由 [#自定义-webhook-路由]

如果您在自己的路由上接收 webhook，而不是使用插件的路由，请使用 `validateWebhookSignature` 验证原始请求体。它会返回一个 promise，因此请使用 `await` 等待结果：

```ts title="app/api/webhooks/creem/route.ts"
import { validateWebhookSignature } from "@creem_io/better-auth/server";

export async function POST(request: Request) {
    const payload = await request.text();
    const signature = request.headers.get("creem-signature");

    const valid = await validateWebhookSignature(
        payload,
        signature,
        process.env.CREEM_WEBHOOK_SECRET!,
    );

    if (!valid) {
        return new Response("Invalid signature", { status: 401 });
    }

    const event = JSON.parse(payload);
    // Handle the verified event

    return new Response(null, { status: 204 });
}
```

不要让 Creem 同时将相同事件发送到这两个路由，否则每个事件都会被处理两次。

## 服务器端辅助函数 [#服务器端辅助函数]

`@creem_io/better-auth/server` 导出了 `createCheckout`、`createPortal`、`cancelSubscription`、`retrieveSubscription`、`searchTransactions` 和 `createCreemClient`，供不通过 Better Auth 端点的代码使用。它们会使用您的 API 密钥调用 Creem，且不会读取会话。请验证请求，并在传入客户或订阅 ID 之前，检查每个 ID 是否属于该用户。

```ts title="actions.ts"
"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) redirect("/sign-in");

    const { url } = await createCheckout(
        {
            apiKey: process.env.CREEM_API_KEY!,
            testMode: process.env.NODE_ENV !== "production",
        },
        {
            productId,
            customer: { email: session.user.email },
            successUrl: "/billing/success",
            metadata: { referenceId: session.user.id },
            skipTrial: session.user.hadTrial === true,
        },
    );

    redirect(url);
}
```

请自行设置 `metadata.referenceId`，以便 webhook 将购买记录关联到用户。

<Callout type="warn">
  在服务器端检查访问权限时，请使用 `auth.api.hasAccessGranted`。导出的 `checkSubscriptionAccess` 和 `getActiveSubscriptions` 辅助函数需要原始查询构建器，而大多数 Better Auth 适配器并不提供这种构建器；此外，它们会忽略预定取消和付款宽限期。
</Callout>

所有辅助函数请参阅 [Creem 服务器 API 文档](https://docs.creem.io/code/sdks/better-auth/server)。

## 防止滥用试用 [#防止滥用试用]

启用持久化后，当订阅进入试用状态时，插件会在用户上设置 `hadTrial`。当该用户登录后开始另一笔结账时，插件会告知 Creem 跳过任何套餐的试用期。只有在服务器辅助函数中传入 `skipTrial` 时，结账才会跳过试用期，如上例所示。

## 架构 [#架构]

启用 `persistSubscriptions`（默认值）后，插件会向您的数据库添加以下内容。设置 `persistSubscriptions: false` 时，不会添加表或字段，且无法使用 `hasAccessGranted` 和试用防护功能。

### 用户 [#用户]

表名：`user`



<DatabaseTable name="user" fields="creemUserTableFields" />

### 订阅 [#订阅]

表名：`creem_subscription`



<DatabaseTable name="creem_subscription" fields="creemSubscriptionTableFields" />

### 自定义 Schema [#自定义-schema]

要重命名表或列，请传入 `schema` 选项。字段映射为列名：

```ts title="auth.ts"
creem({
    // ... other options
    schema: {
        creem_subscription: {
            modelName: "billing_subscription",
            fields: { referenceId: "user_id" },
        },
        user: {
            fields: { creemCustomerId: "creem_customer_id" },
        },
    },
});
```

## 选项 [#选项]

### 服务端 [#服务端]

| 选项                     | 类型         | 描述                                                      |
| ---------------------- | ---------- | ------------------------------------------------------- |
| `apiKey`               | `string`   | 你的 Creem API 密钥。&#x2A;*必填。**                            |
| `webhookSecret`        | `string`   | Webhook 签名密钥。仅在设置此项时才会注册 Webhook 路由。                    |
| `testMode`             | `boolean`  | 使用 Creem 测试 API。默认值：`false`。                            |
| `defaultSuccessUrl`    | `string`   | 结账的默认 `successUrl`。接受相对或绝对 URL。                         |
| `persistSubscriptions` | `boolean`  | 在数据库中存储客户和订阅。默认值：`true`。                                |
| `schema`               | `object`   | 重命名插件的表和列。参见[自定义 Schema](#customizing-the-schema)。      |
| `onGrantAccess`        | `function` | 当订阅事件授予访问权限时调用。参见[授予和撤销访问权限](#grant-and-revoke-access)。 |
| `onRevokeAccess`       | `function` | 当订阅事件撤销访问权限时调用。                                         |

各事件回调列于[事件回调](#event-callbacks)中。

### 客户端 [#客户端]

| 选项                     | 类型        | 描述                                                                          |
| ---------------------- | --------- | --------------------------------------------------------------------------- |
| `persistSubscriptions` | `boolean` | 当服务端禁用持久化时设为 `false`，这样会话类型就不会包含 `creemCustomerId` 和 `hadTrial`。默认值：`true`。 |

## 资源 [#资源]

* [Creem Better Auth 文档](https://docs.creem.io/code/sdks/better-auth)
* [2.0 迁移指南](https://docs.creem.io/code/sdks/better-auth/migration)
* [插件源码](https://github.com/armitage-labs/creem/tree/main/packages/integrations/better-auth)和[问题跟踪器](https://github.com/armitage-labs/creem/issues)
* [Creem 控制面板](https://creem.io/dashboard)，提供应用内在线聊天
* [support@creem.io](mailto:support@creem.io)
* [Creem Discord 社区](https://discord.gg/q3GKZs92Av)

