Commet

使用 Commet 进行计费和订阅的更佳认证插件

Commet 是适用于 SaaS 和 AI 产品的计费和支付解决方案。作为代理商(Merchant of Record),Commet 处理订阅、基于使用的计费、税务合规和全球支付——让您几分钟内即可开始实现盈利。

此插件由 Commet 团队维护。如遇错误、问题或功能请求, 请联系Commet 支持

功能

  • 注册时自动创建客户
  • 客户门户用于自助式账单管理
  • 订阅管理(获取、更改计划、取消)
  • 功能访问控制(布尔值、计量、席位)
  • 计量计费的使用跟踪
  • 按用户定价的席位管理
  • 可选的带签名验证的 webhook 处理

安装

pnpm add better-auth @commet/better-auth @commet/node

准备工作

Commet 控制面板 获取您的 API 密钥。

.env
COMMET_API_KEY=ck_...
COMMET_ENVIRONMENT=sandbox # 或 production

服务器配置

auth.ts
import { betterAuth } from "better-auth";
import {
  commet,
  portal,
  subscriptions,
  features,
  usage,
  seats,
} from "@commet/better-auth";
import { Commet } from "@commet/node";

const commetClient = new Commet({
  apiKey: process.env.COMMET_API_KEY,
  environment: process.env.COMMET_ENVIRONMENT, // 'sandbox' 或 'production'
});

export const auth = betterAuth({
  // ... 您的配置
  plugins: [
    commet({
      client: commetClient,
      createCustomerOnSignUp: true,
      use: [
        portal(),
        subscriptions(),
        features(),
        usage(),
        seats(),
      ],
    }),
  ],
});

客户端配置

auth-client.ts
import { createAuthClient } from "better-auth/react";
import { commetClient } from "@commet/better-auth";

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

配置选项

commet({
  client: commetClient,                    // 必填:Commet SDK 实例
  createCustomerOnSignUp: true,            // 注册时自动创建客户
  getCustomerCreateParams: ({ user }) => ({
    legalName: user.name,
    metadata: { source: "web" },
  }),
  use: [/* 插件列表 */],
})

启用 createCustomerOnSignUp 后,Commet 会自动创建一个客户,externalId 设置为用户 ID,无需数据库映射。

门户插件

将用户重定向至 Commet 客户门户,进行自助账单管理。

Server
import { commet, portal } from "@commet/better-auth";

commet({
  client: commetClient,
  use: [
    portal({ returnUrl: "/dashboard" }),
  ],
})
Client
// 跳转至 Commet 客户门户
await authClient.customer.portal();

订阅插件

管理客户订阅。

Server
import { commet, subscriptions } from "@commet/better-auth";

commet({
  client: commetClient,
  use: [subscriptions()],
})
Client
// 获取当前订阅
const { data: subscription } = await authClient.subscription.get();

// 更改计划
await authClient.subscription.changePlan({
  subscriptionId: "sub_xxx",
  planCode: "enterprise",
  billingInterval: "yearly",
});

// 取消订阅
await authClient.subscription.cancel({
  subscriptionId: "sub_xxx",
  reason: "太贵了",
  immediate: false, // 期末取消
});

功能插件

检查认证用户的功能访问权限。

Server
import { commet, features } from "@commet/better-auth";

commet({
  client: commetClient,
  use: [features()],
})
Client
// 列出所有功能
const { data: featuresList } = await authClient.features.list();

// 获取特定功能
const { data: feature } = await authClient.features.get("api_calls");

// 检查功能是否启用(布尔值)
const { data: check } = await authClient.features.check("sso");

// 检查用户是否可以多使用一个单位(计量)
const { data: canUse } = await authClient.features.canUse("api_calls");
// 返回: { allowed: boolean, willBeCharged: boolean }

使用插件

跟踪计量计费的使用事件。

Server
import { commet, usage } from "@commet/better-auth";

commet({
  client: commetClient,
  use: [usage()],
})
Client
await authClient.usage.track({
  eventType: "api_call",
  value: 1,
  idempotencyKey: `evt_${Date.now()}`,
  properties: { endpoint: "/api/generate" },
});

认证用户会自动关联到事件。

席位插件

管理基于席位的许可证。

Server
import { commet, seats } from "@commet/better-auth";

commet({
  client: commetClient,
  use: [seats()],
})
Client
// 列出所有席位余额
const { data: seatBalances } = await authClient.seats.list();

// 添加席位
await authClient.seats.add({ seatType: "member", count: 5 });

// 移除席位
await authClient.seats.remove({ seatType: "member", count: 2 });

// 设置精确数量
await authClient.seats.set({ seatType: "admin", count: 3 });

// 一次设置所有席位类型数量
await authClient.seats.setAll({ admin: 2, member: 10, viewer: 50 });

Webhooks 插件(可选)

处理 Commet webhooks。此功能可选,因为您始终可以直接查询状态。

Server
import { commet, webhooks } from "@commet/better-auth";

commet({
  client: commetClient,
  use: [
    webhooks({
      secret: process.env.COMMET_WEBHOOK_SECRET,
      onPayload: (payload) => {
        // 通用处理器
      },
      onSubscriptionCreated: (payload) => {},
      onSubscriptionActivated: (payload) => {},
      onSubscriptionCanceled: (payload) => {},
      onSubscriptionUpdated: (payload) => {},
    }),
  ],
})

在 Commet 控制面板中配置 webhook 端点:/api/auth/commet/webhooks

完整示例

auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import {
  commet as commetPlugin,
  portal,
  subscriptions,
  features,
  usage,
  seats,
} from "@commet/better-auth";
import { Commet } from "@commet/node";
import { db } from "./db";
import * as schema from "./schema";

const commetClient = new Commet({
  apiKey: process.env.COMMET_API_KEY!,
  environment: process.env.COMMET_ENVIRONMENT as "sandbox" | "production",
});

export const auth = betterAuth({
  database: drizzleAdapter(db, { provider: "pg", schema }),
  emailAndPassword: { enabled: true },
  plugins: [
    commetPlugin({
      client: commetClient,
      createCustomerOnSignUp: true,
      getCustomerCreateParams: ({ user }) => ({
        legalName: user.name,
      }),
      use: [
        portal({ returnUrl: "/dashboard" }),
        subscriptions(),
        features(),
        usage(),
        seats(),
      ],
    }),
  ],
});
auth-client.ts
import { createAuthClient } from "better-auth/react";
import { commetClient } from "@commet/better-auth";

export const authClient = createAuthClient({
  baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL,
  plugins: [commetClient()],
});

export const { signIn, signUp, signOut, useSession } = authClient;
dashboard.tsx
"use client";

import { authClient } from "@/lib/auth-client";

export function BillingSection() {
  const handlePortal = async () => {
    await authClient.customer.portal();
  };

  const checkFeature = async () => {
    const { data } = await authClient.features.canUse("api_calls");
    if (data?.allowed) {
      // 执行操作
      await authClient.usage.track({ eventType: "api_call" });
    }
  };

  return (
    <div>
      <button onClick={handlePortal}>管理账单</button>
      <button onClick={checkFeature}>使用功能</button>
    </div>
  );
}