金融与交易
使用 Clerk Billing 实现 SaaS 订阅…
在 Clerk 应用中启用 Billing、配置计划和功能,并用 PricingTable、has() 与 billing webhook 实现订阅与权限门控时使用。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 60/100 ×0.15
适用场景
使用 Clerk Billing 实现 特定
解决思路
- 固定 `@clerk/nextjs` 和 `clerk-js` 包版本;Billing API 仍是实验性,支持版本表见 `clerk` skill。
- 启用 Billing:访问 `https://dashboard.clerk.com/last-active?path=billing/settings` 或运行 `clerk enable billing`;开发实例可用共享 Clerk development gateway,生产实例需 Stripe 账号处理支付;未启用时 dev 抛 `cannot_render_billing_disabled`,prod 渲染为空。
- 创建 plans:访问 `https://dashboard.clerk.com/last-active?path=billing/plans`;User Plans 对应 `<PricingTable />`(默认 `for="user"`),Organization Plans 对应 `<PricingTable for="organization" />`;slug 按 tab 作用域,创建后不可移动;plans 存于 Clerk,不同步到 Stripe。
- 在 plan 内添加 features:打开 Dashboard → Billing → Plans,点一个 plan,使用其 Features 区;features 按 plan 作用域,非全局;同一 slug 可附加到多个 plans;`has({ feature: 'export' })` 在活动 plan 包含该 slug 时匹配。
- 渲染 `<PricingTable />`;B2B 传 `for="organization"`;它会自动渲染 Dashboard 中配置的所有 plans,选择 plan 打开 Clerk 应用内 checkout drawer,基本用法无需 props。
- 用来自 `auth()` 的 `has({ plan })` 或 `has({ feature })` 门控访问。
- 处理 billing webhooks 管理订阅生命周期。
- 使用 Dashboard 快捷入口:启用 Billing:`https://dashboard.clerk.com/last-active?path=billing/settings`;创建/编辑 plans:`https://dashboard.clerk.com/last-active?path=billing/plans`;Membership mode(B2C + B2B 共存):`https://dashboard.clerk.com/last-active?path=organizations-settings`;编辑 features:Plans → 点击 plan → Features 区(无直接 URL)。
- 通过 PLAPI 编程配置 billing:预关联项目:运行 `clerk auth login` 和 `clerk link`;Billing 仅账户可用,不能未认领 app 上启用,先 claim app。
- 用 CLI 启用 Billing:`clerk enable billing` 默认两个 target 并自动创建 `free_user` + `free_org` plans;`clerk enable billing --for org` 仅组织;`clerk enable billing --for user` 仅用户。
- 拉取当前 billing config:`clerk config pull --keys billing > billing.json`,写入当前链接实例的 billing 配置(toggles + plans + features)到 `billing.json`。
- 编辑并应用 billing config:编辑 `billing.json` 增删 plans 或 features,然后 `clerk config patch --file billing.json --dry-run` 预览 diff,`clerk config patch --file billing.json` 应用;传 `--instance prod` 目标生产实例。
- 使用 raw PATCH 一次性更新 plans/features:运行 `clerk api --platform PATCH /v1/platform/applications/<app_id>/instances/<ins_id>/config -d ...`,在 `billing` 中提供 `plans`(slug、name、amount、currency、payer_type、is_recurring)和 `features`(slug、name)。
- 注意编程配置只处理 billing config(toggles + plans + features catalog);订阅生命周期(用户选 plan、checkout、续订、取消)仍走 `<PricingTable />` + billing webhooks,见 `clerk-webhooks`。
- 顶层 `features` map 操作和 plan-feature attachments 通过 PLAPI billing config handler 完全支持。
- 按任务查阅参考:`references/billing-components.md` 查 `<PricingTable />` props、`<CheckoutButton />`、`<Show>` billing patterns;`references/b2c-patterns.md` 查 B2C;`references/b2b-patterns.md` 查 B2B;`references/billing-webhooks.md` 查 webhook 事件目录、payload、handler 模板。
- 选择 feature 还是 plan:门控特定能力用 `has({ feature: 'slug' })`(如 export、analytics、API access、audit logs);门控 tier 用 `has({ plan: 'slug' })`(如 pro dashboard、org subscription level、重定向 free 用户)。
- 场景检查:Gate "Export CSV" 按钮用 `has({ feature: 'export' })`;Gate "Analytics" 区用 `has({ feature: 'analytics' })`;Gate 整个 `/dashboard/pro` 用 `has({ plan: 'pro' })`;检查 org 是否有 team 订阅用 `has({ plan: 'org:team' })`;Gate SSO 配置用 `has({ feature: 'sso' })`。
- 当用户说“gate the export feature”或“gate analytics”,总是用 `has({ feature })`;只有 gate 是 plan tier 本身才用 `has({ plan })`。
- 渲染定价表:在页面导入 `PricingTable`(来自 `@clerk/nextjs`)并返回 `<PricingTable />`;B2B 传 `for="organization"` 渲染 org-level plans。
- 服务端检查 feature entitlements:在 Server Component 中 `const { has } = await auth()`,用 `has({ feature: 'analytics' })`、`has({ feature: 'export' })` 条件渲染;features 在 Dashboard → Billing → Features 配置并分配到 plans;门控细粒度能力用 feature 不用 plan。
- 客户端检查 feature entitlements:Client Component 使用 `useAuth()`,`if (!isLoaded) return null`,用 `has?.({ feature: 'export' })` 条件渲染;Server Components 用 `auth()`,Client Components 用 `useAuth()`,两者都支持 `has({ feature })` 和 `has({ plan })`。
- 服务端检查订阅 plan:用 `auth()` 获取 `has`,若 `!has({ plan: 'pro' })` 则 `redirect('/pricing')`;用于 tier 级门控,不是单个 feature。
- 客户端 plan 检查:用 `useAuth()` hook,若 `has?.({ plan: 'pro' })` 返回 null,否则显示升级提示和定价页链接。
- B2B 按席位计费:org plans 可带 seat limit(成员上限),Clerk 在 invite 时执行;org 侧 plan 检查用 `org:` slug 前缀,如 `has({ plan: 'org:team' })`;渲染 B2B 定价页用 `<PricingTable for="organization" />`;组织账户 billing UI 用 `<OrganizationProfile />`;详见 `references/b2b-patterns.md` 的 tiered plan naming、seat-limit invariants、admin-only billing、webhook handlers。
- 显示订阅状态:用 `has({ plan })` 检查特定 plans,或在 client components 用 `useSubscription()` 获取完整订阅详情;不要直接从 `sessionClaims` 读取 plan 信息。
- 服务端显示当前 plan:用 `auth()` 的 `has` 依次检查 pro、starter,否则 free,并显示;free 时显示升级链接。
- 客户端完整订阅详情:用 `useSubscription()`(来自 `@clerk/nextjs/experimental`),处理 isLoading 和空订阅;显示 `subscription.status`、`subscription.nextPayment.date`。`useSubscription()` 仅用于展示;授权检查始终用 `has({ plan })` 或 `has({ feature })`。
- 按 plan 保护 API routes:在 route handler 中用 `auth()` 的 `has`,若 `!has({ plan: 'pro' })` 返回 403 JSON。
- 处理 billing webhooks:使用 Clerk 事件名而非 Stripe 事件名;Clerk 用 dot-notation 和 camelCase;没有 `subscription.canceled`,取消在 item 级触发 `subscriptionItem.canceled`。
- Webhook 事件映射:`customer.subscription.created` → `subscription.created`;`customer.subscription.updated` → `subscription.updated`;订阅 active → `subscription.active`;订阅 past due → `subscription.pastDue`;订阅项 canceled → `subscriptionItem.canceled`;订阅项 past due → `subscriptionItem.pastDue`;订阅项 updated → `subscriptionItem.updated`;订阅项 active → `subscriptionItem.active`;订阅项 upcoming renewal → `subscriptionItem.upcoming`;订阅项 ended → `subscriptionItem.ended`;订阅项 abandoned → `subscriptionItem.abandoned`;订阅项 expired → `subscriptionItem.expired`;订阅项 incomplete → `subscriptionItem.incomplete`;free trial ending soon → `subscriptionItem.freeTrialEnding`;payment attempt created → `paymentAttempt.created`;payment attempt updated → `paymentAttempt.updated`;始终在 `evt.type` 检查中使用 Clerk 事件名。
- Webhook payload shape:payload 嵌套;订阅实体在 `evt.data.payer`(`user_id?`、`organization_id?`);plan info 在每个 item 的 `evt.data.items[i].plan.slug`;subscription id 是 `evt.data.id`;subscription items 不带 `subscription_id` 反向引用,因此在 `subscriptionItem.*` handler 中用 item id(`evt.data.id`)标识记录,或按 payer 加 plan 查找。
- 最小 webhook handler:从 `@clerk/nextjs/webhooks` 导入 `verifyWebhook`;POST 中 verify,失败返回 400;分支 `subscription.created`,解构 `{ id, payer, items, status }`,`entityId = payer.organization_id ?? payer.user_id`,`plan = items[0]?.plan?.slug`,调用 `db.subscriptions.upsert`,where `subscriptionId: id`,create/update 包含 subscriptionId、entityId、plan、status;按事件目录添加更多分支;返回 200;完整模板见 `references/billing-webhooks.md`。
- 升级/降级流程:在应用内 billing 页面用 `auth()` 判断 `isPro = has({ plan: 'pro' })`;无论是否 Pro 都渲染 `<PricingTable />`;它会针对已订阅用户显示当前 plan 并允许升级或取消,通过 Clerk 应用内 checkout drawer。
- Plan 和 feature 命名:在 Clerk Dashboard → Billing 定义 plan slugs 和 feature slugs;使用小写 slug 匹配 dashboard。
- 常见命名约定:Free 无需 plan check,仅 basic features;Starter slug `starter`,features `analytics`、`api_access`;Pro slug `pro`,features `analytics`、`export`、`team`;Enterprise slug `enterprise`,features all + `sso`、`audit_logs`。
- 选择 B2B 还是 B2C 计费:B2C SaaS 由个人用户订阅,在 user session 上用 `has({ plan: 'pro' })`;B2B SaaS 由组织订阅,在 org session 上用 `has({ plan: 'org:team' })`;seat-limited B2B 由组织订阅,plan 有 seat cap,定价按 plan 而非按成员,为更大组织分层 plan。
- B2B 确保用户有 active org session;`has()` 评估 active entity(user 或 org)。
- Checkout flows:Clerk 通过 `<PricingTable />` 和 `<CheckoutButton />` 自动渲染自己的 checkout drawer;plans 和 pricing 存在 Clerk;若需从 server action 触发 checkout,重定向到渲染 `<PricingTable />` 的页面,例如 `redirect('/pricing')`。
- 错误签名快速诊断:`cannot_render_billing_disabled`(dev)→ 在 Dashboard 启用 Billing 或运行 `clerk enable billing`。
- `<PricingTable />` 渲染为空 → 创建 plan 在匹配 tab;B2B 传 `for="organization"`;检查 Billing Settings。
- 用户无法在 B2C + B2B 应用订阅个人 plan → Membership required mode(默认自 2025-08-22)禁用个人账户,强制 choose-organization;若需个人 + 组织订阅共存,Dashboard → Organizations settings → Membership optional。
- 找不到 Features 页 → Features 按 plan 不是全局;Dashboard → Billing → Plans → 点击 plan → Features。
- 成功 checkout 后 `has({ plan: 'pro' })` 总返回 false → session token 未刷新;`await clerk.session?.reload()` 或导航强制新 session。
- 订阅前 `has({ plan: 'pro' })` 返回 false → plan slug 大小写不匹配,或 Billing 未启用,或生产支付网关未连接;在 Dashboard → Billing → Plans 验证 slug;确认 Billing → Settings 显示 enabled + connected gateway。
- `has({ permission: 'org:x:y' })` 对拥有角色的用户返回 false → 与该 permission 关联的 Feature 未包含在组织 active Plan;在 Dashboard → Billing → Plans → Features 添加 Feature 到 Plan。
- Webhook 401 / signature verification failed → `CLERK_WEBHOOK_SIGNING_SECRET` 不匹配或路由被 middleware 保护;从 Dashboard → Webhooks 复制 Signing Secret;将 webhook route 加入 `createRouteMatcher(['/api/webhooks(.*)'])`。
- Billing gates permissions:当 Billing 启用时,即使 user 通过角色被分配权限,如果与权限关联的 Feature 未包含在组织 active Plan,`has({ permission: 'org:posts:edit' })` 返回 false;这是设计;始终确保所需 Feature 附加到 Plan 在 Dashboard → Billing → Plans → Features。
- 参考其他技能:`clerk-setup` 初始 Clerk 安装;`clerk-orgs` B2B organizations(B2B billing 和 seat-limit plans 必需);`clerk-webhooks` webhook signature verification 和 routing。
适用边界 · 注意事项
- 不适用于未使用 Clerk 认证/计费、或不能启用 Clerk Billing 的项目。
- 不适用于仅使用 Stripe 原生订阅且不使用 Clerk PricingTable、has() 或 billing webhooks 的场景。
- 生产环境没有 Stripe 账号或未连接支付网关时,不应依赖 Clerk Billing 的生产支付流程。