Stripe Entitlements 上线前最容易踩的坑,是把它当成产品里的全部权限系统。它更适合做「付费状态 -> 当前 feature 权益」这一层事实源;真正的运行时开关、席位占用、人工补偿、灰度开关和客服备注,仍然要落在你自己的数据库里。
2026 年 5 月 27 日查看 Stripe 文档时,Entitlements 的官方路径已经很清楚:创建 Feature,把 Feature attach 到 Product,客户订阅对应 Product 后,Stripe 会生成 Active Entitlement;订阅创建、升级、降级或取消时,entitlements.active_entitlement_summary.updated webhook 会推送最新权益摘要。一个人 SaaS 先把这条链路跑稳,比一次性重写整个 billing 后台更现实。
什么东西交给 Stripe,什么留在产品库?
Stripe Billing 管钱和订阅生命周期,Stripe Entitlements 管客户当前被授予哪些 feature。产品库要管「用户点击按钮时能不能进」以及「客服为什么给他开了三天补偿」。
最小分层可以这样定:
| 对象 | 放在 Stripe | 放在产品库 | 上线前判断 |
|---|---|---|---|
| 套餐商品 | Product、Price、Subscription | plan_code、展示文案、排序 | Product 是付费容器,不等于页面文案 |
| 功能权益 | Feature、Product Feature、Active Entitlement | feature_code、运行时开关、默认限制 | Stripe 给付费来源,产品决定入口开关 |
| 席位数量 | Subscription Item quantity | seat_limit、已占席位、邀请状态 | quantity 只能做输入,不能代替席位表 |
| 试用和补偿 | Trial、discount、credit 或手工订阅调整 | manual_grant、到期时间、操作者 | 客服补偿必须能解释来源 |
| 查账状态 | Customer、Subscription、Invoice、Entitlements | entitlement snapshot、event log、客服备注 | 两边状态不一致时要有重放入口 |
默认建议:Stripe Entitlements 只写入「当前可用 feature 列表」,不要把它设计成每次请求都同步查 Stripe。Stripe 文档也建议把 entitlements 持久化到内部系统,这样授权判断更快,webhook 失败时也能补拉。
功能开关命名怎么定,lookup_key 别改成营销文案
Feature 的 lookup_key 是产品权限里最硬的字段。Stripe API 创建 Feature 时要求提供唯一 lookup_key,Dashboard 创建后不能编辑 lookup key;它不应该叫 pro-plan-best-value,而应该叫 api_access、team_seats、advanced_reports 这种能长期留在代码里的名字。
一套稳定命名可以分三层:
| 权限层 | 示例 key | 谁会用 | 命名规则 |
|---|---|---|---|
| 能力型 feature | api_access | 后端鉴权、菜单显示 | 名字描述能力,不描述套餐 |
| 限额型 feature | monthly_exports | 任务队列、用量页 | 另配本地限制值,不只靠 Entitlements |
| 服务型 feature | priority_support | 客服系统、工单路由 | 和客服 SLA 文案分开 |
| 席位型 feature | team_workspace | 邀请成员、组织设置 | 席位数来自 subscription item 或本地表 |
| 内部灰度 | new_editor_beta | 实验系统 | 不放 Stripe,放产品 feature flag |
Stripe Entitlements 的 Feature name 是内部用途,不是给客户展示的价格页文案。价格页可以写「团队协作」或「高级报表」,代码和 webhook 里仍然读 team_workspace、advanced_reports。
套餐和 feature 怎么映射?
不要从「有几个套餐」开始建 Feature。更稳的顺序是先列产品能力,再把能力 attach 到不同 Product。Stripe 的 Product Feature 表示 Feature 和 Product 之间的挂接关系,同一个 Feature 可以分配给多个 Product。
一个常见 B2B SaaS 可以这样落:
| SaaS 计划 | Stripe Product | Price / Subscription Item | Stripe Feature lookup_key | 产品侧限制 |
|---|---|---|---|---|
| Free | prod_free | 无付费订阅或 0 元 Price | 不依赖 Entitlements | 1 个 workspace,100 条记录 |
| Starter | prod_starter | $19/month base item | api_access、basic_reports | 3 个 seat,1,000 次 API |
| Pro | prod_pro | $49/month base item | api_access、advanced_reports、webhook_outbound | 10 个 seat,10,000 次 API |
| Team | prod_team | base item + seat item quantity | api_access、advanced_reports、webhook_outbound、team_workspace、priority_support | seat 数按 item quantity,支持 SSO 白名单 |
这里有两个细节。第一,seat quantity = 10 不应该生成十个 Feature;它只是 team_workspace 这个 feature 的限制值。第二,Free 计划不一定要走 Entitlements;未付费用户的默认权限可以直接来自本地 plan_code=free,否则会把授权链路拉得太长。
如果你已经有 plans 表,建议加一张映射表,而不是把 Stripe ID 写死在代码里:
create table plan_feature_map (
plan_code text not null,
stripe_product_id text not null,
stripe_feature_lookup_key text not null,
local_feature_code text not null,
limit_json jsonb not null default '{}',
active boolean not null default true,
primary key (plan_code, local_feature_code)
);
limit_json 可以放 { "seat_limit": 10, "api_calls_monthly": 10000 }。Stripe 告诉你客户有 team_workspace,产品库再决定这个 workspace 能邀请几个人。
迁移老订阅时,哪些客户不能一起切?
老 SaaS 往往已经有 plan=pro、is_premium=true、max_projects=20 这类本地权限。接 Stripe Entitlements 时,不要第一晚把所有客户都切到 webhook 驱动;先把老状态和 Stripe 当前状态并排展示,找出不能自动迁移的客户。
迁移批次可以按风险分:
| 客户类型 | 迁移动作 | 暂停条件 |
|---|---|---|
| 新客户 | Checkout 后直接走 Entitlements + webhook | checkout.session.completed 有订单但无 entitlement snapshot |
| 正常月付老客户 | 保留 legacy 权限;等下一账期、订阅变更或补拉验证后再生成新快照 | 本地 plan 和 Stripe Product 对不上,或 Active Entitlements 为空 |
| 年付老客户 | 保留原权益到当前 period end,再切到 Product Feature | 曾经人工赠送过额外功能 |
| 企业合同客户 | 手工映射 feature 和限制值 | 合同里有非公开权益、PO 或特殊 SLA |
| 退款或争议客户 | 不自动迁移,先冻结自动开通脚本 | Invoice、Subscription、客服备注三边不一致 |
Stripe Entitlements 文档说明,对已有订阅添加或移除 Product Feature 时,相关 active entitlement 变化会在下一账期开始时生效。迁移老客户时不要把「现在查不到 Active Entitlement」解释成应该关权限;先保留 legacy 或本地 override,等续费、订阅变更、补拉验证或人工批次确认后,再把运行时判断切到新快照。
Stripe 订阅可以修改,不必取消重建;但改 Price、数量、计费周期、增删 subscription items 可能触发 proration 或新 invoice。迁移时把「计费变化」和「权限变化」拆开:先让 Product Feature 映射稳定,再处理套餐升级、降级和账单影响。
一个实用做法是加双读开关:
function canUseFeature(user, featureCode) {
if (user.entitlements_v2_enabled && user.entitlements_v2_verified) {
return entitlementSnapshot.has(featureCode);
}
return legacyPlanRules.allow(user.plan_code, featureCode);
}
双读期里,客服页同时显示 legacy plan、Stripe Product、Active Entitlement、entitlements_v2_verified 和最后一个 webhook event id。差异超过 24 小时还没解释清楚,就不要继续扩大迁移批次;老客户默认保留原产品访问,不在账期中途因为新快照缺失而降级。
客服查账要看哪三张表?
客户说「我付费了,为什么看不到高级报表」,客服不要只看 Stripe Dashboard 的 invoice 是否 paid。正确问题是四个:付款有没有成功,订阅状态是否允许开通,Stripe 有没有 active entitlement,本地权限快照有没有写入。
最小查账表建议三张:
| 表名 | 关键字段 | 客服能回答什么 |
|---|---|---|
billing_customers | user_id、stripe_customer_id、default_workspace_id | 这个用户对应哪个 Stripe Customer |
entitlement_snapshots | stripe_customer_id、lookup_key、source_event_id、synced_at、active | 当前哪些功能已经开通或撤销 |
billing_event_log | event_id、event_type、subscription_id、invoice_id、processed_at、status、error | webhook 是否到达、是否处理失败 |
客服页面上不要只显示绿色或红色。要显示「Stripe 侧看到的权益」和「产品侧实际启用的权益」。如果 advanced_reports 在 Stripe Active Entitlements 里存在,但产品侧菜单仍关闭,问题通常在 webhook 处理器、缓存刷新或 workspace 绑定上。
推荐客服查询顺序:
- 用用户邮箱找到
stripe_customer_id。 - 调 Stripe List Active Entitlements,确认
lookup_key列表。 - 打开本地
entitlement_snapshots,看source_event_id和synced_at。 - 查
billing_event_log里同一个 event 是否处理失败。 - 如果是 seat 问题,再查 subscription item quantity 和已占席位表。
这套顺序不会给税务、收入确认或合同解释下结论,只解决产品访问和客服查账。
Stripe Dashboard 多人操作怎么留痕?
一个人公司也会出现多人操作:创始人改 Product,兼职客服查发票,工程师重放 webhook,外包开发看日志。Stripe Dashboard、部署后台、数据库和客服系统分散在不同环境时,最怕同一笔订单被两个人各改一次。
涉及 Product Feature、Subscription、Customer 和 webhook replay 的动作,建议固定后台负责人,并用Stripe Dashboard 稳定访问承载核心后台操作。它只是让后台操作环境更可控,不能替代 Stripe 的身份验证、真实订单记录、权限分配和工单证据。
团队内部可以把高风险动作拆成三类:
| 动作 | 谁能做 | 必留字段 |
|---|---|---|
| attach / remove Product Feature | 负责 billing 的工程师 | product id、feature lookup_key、操作者、变更原因 |
| 手工补开权益 | 客服负责人 + 工程确认 | user id、feature、到期时间、ticket id |
| 重放 webhook | 工程师 | event id、重放时间、处理结果、错误信息 |
如果使用 Customer Portal 给用户自助管理订阅,也不要把 Portal 当成权限事实源。Portal 解决客户侧账单和订阅管理入口,产品开关仍以 webhook 和本地 snapshot 为准。
审计流水要记到什么粒度?
这里的审计流水指产品内部事故回放,不是会计或合规承诺。目标很简单:三个月后客户问为什么 4 月 12 日失去了 priority_support,你能把 Subscription、Active Entitlement、本地快照和人工动作串起来。
建议记录五类事件:
| 事件 | 触发来源 | 最少字段 |
|---|---|---|
entitlement_snapshot_refreshed | webhook 或手工补拉 | customer、lookup_key 列表、source event id |
feature_access_granted | 本地权限服务 | user、workspace、feature、reason |
feature_access_revoked | 本地权限服务 | user、workspace、feature、reason |
manual_entitlement_override | 客服或工程后台 | operator、ticket、expires_at、old value、new value |
subscription_quantity_changed | Stripe subscription item | subscription item id、old quantity、new quantity |
entitlements.active_entitlement_summary.updated 的摘要 payload 里最多只有 10 条 entitlements。客户权益多于 10 条时,要按 payload 里的 URL 或 List Active Entitlements API 拉完整分页列表,再把 snapshot 覆盖成同一个版本。否则客服页可能只看到前 10 个功能,误判成权限丢失。
哪些事情别交给 Entitlements?
Entitlements 不适合处理所有付费例外。运行时 kill switch、内部灰度、黑名单、企业合同特殊条款、一次性人工补偿、税务处理、收入确认和合规判断,都不要塞进 Stripe Feature。
也不要把营销功能和真实权限混在一起。Stripe Product 支持 pricing page 上的 marketing features,但那是展示给客户看的卖点;Entitlements Feature 是系统里可授权的能力。价格页写「10x productivity」没有问题,权限表里应该只有 workflow_automation 或 bulk_export。
上线前的停止条件很明确:
| 信号 | 为什么要停 | 下一步 |
|---|---|---|
| lookup_key 还在频繁改 | 代码、webhook 和客服表都会被牵连 | 冻结命名表,先只改前台文案 |
| 老客户手工权益很多 | 自动迁移会误伤合同或补偿 | 单独建 override 表 |
| webhook 没有幂等 | 重放可能重复开通或撤销 | 给 event id 和 customer-feature 加唯一约束 |
| 客服只能看 invoice | 付费成功不等于权限写入成功 | 补 entitlement snapshot 页面 |
| 席位只存在 Stripe quantity | 无法解释已占席位和邀请状态 | 建 workspace seat 表 |
等这几项补齐后,再把新客户全量切到 Entitlements。老客户继续按批次迁移,发现 Stripe 和本地状态不一致时,以保留现有产品访问为默认动作,再让工程查账,不要在客服窗口里现场改套餐。