使用 Stripe 管理订阅:升降级、取消与 Supabase Edge Function Webhook
本文用分步方式说明如何用 Stripe 处理 订阅(Subscription),包括:套餐升级/降级、取消订阅,以及用 Supabase Edge Function 作为后端接收 Webhook、同步业务库(例如用户权益)。适合已能部署 Edge Function、熟悉 REST/环境变量的开发者。
你将完成什么
- 在 Stripe 中配置产品与价格(按月/按年等)
- 客户端或自建结账页创建 Checkout Session / Customer Portal,完成首次订阅
- 通过 Stripe API 或 Customer Portal 实现升级、降级、取消
- 部署 Supabase Edge Function,校验 Webhook 签名并处理
customer.subscription.*等事件 - (建议)在数据库中维护订阅状态,与 Supabase Auth 用户关联
核心概念(先对齐名词)
| 概念 | 说明 |
|---|---|
| Product | 逻辑上的「产品」,如「Pro 会员」 |
| Price | 具体计费单元,绑定币种、周期(recurring) |
| Customer | Stripe 侧的客户,建议与你的业务用户一一对应(可用 metadata 存 user_id) |
| Subscription | 客户对某个 Price 的周期性合约;升级/降级本质是换 subscription.items 里的 Price |
| Checkout | 托管结账页,适合首次开通 |
| Customer Portal | Stripe 托管的「管理订阅」页面,用户自助换套餐、取消、更新支付方式 |
| Webhook | Stripe 向你的服务器推送事件(支付成功、订阅更新等),必须以服务端验签 |
前置条件
- Stripe 账号(先用测试模式)
- Supabase 项目,已安装 Supabase CLI
- 本地已登录 CLI:
supabase login,项目已supabase link
第一步:在 Stripe 中创建产品与价格
- 登录 Stripe Dashboard,右上角切换到 Test mode。
- 进入 Product catalog → Add product。
- 填写产品名称(如「专业版」),添加 Pricing:
- Recurring:选择计费周期(月付 / 年付等)。
- 可创建多个 Price(例如月付价、年付价),它们都属于同一或不同 Product,按你的套餐设计即可。
- 记下每个 Price ID(形如
price_xxx),后续 API 与 Webhook 里会用到。
升降级设计建议:每个「档位」对应一个 Price。升级/降级 = 把订阅行项目从 Price A 换成 Price B(可在换档时选择按比例计费
proration_behavior,见下文)。
第二步:把 Stripe 客户与业务用户关联
常见做法:
- 用户在你的 App 登录后(例如 Supabase Auth 的
user.id)。 - 首次需要付款时,在服务端(Edge Function 或已有后端)调用 Stripe Customers API 创建客户,并在
metadata写入supabase_user_id(或你的主键)。 - 把返回的
customer.id(cus_xxx)存到你自己的表(如profiles.stripe_customer_id),下次直接复用。
不要在浏览器暴露 Secret Key;创建 Customer、创建 Checkout Session 等应放在 Edge Function 或受信任后端。
第三步:首次订阅 — 使用 Checkout Session(推荐)
在 Edge Function 中(示例逻辑,非完整可运行文件):
- 接收已认证用户的请求(例如校验 Supabase JWT)。
- 读取该用户的
stripe_customer_id;若无则先创建 Customer 并落库。 - 调用
stripe.checkout.sessions.create:mode: 'subscription'customer或customer_emailline_items: [{ price: 'price_xxx', quantity: 1 }]success_url/cancel_url指向你的前端页面client_reference_id或metadata可带user_id,便于 Webhook 关联。
- 把返回的
session.url返回给客户端,重定向用户到该 URL 完成支付。
用户支付成功后,Stripe 会创建 Subscription,后续变更都针对该订阅对象进行。
第四步:套餐升级与降级
有两种主流实现方式:
方式 A:Customer Portal(最少代码)
- 在 Stripe Dashboard → Settings → Billing → Customer portal 中启用门户,并配置允许的操作:切换方案(Switch plans)、取消 等。
- 在 Edge Function 中创建 Billing Portal Session:
stripe.billingPortal.sessions.create,传入customer与return_url。 - 前端拿到 URL 后跳转,用户在 Stripe 页面选择新套餐即完成换档。
优点:省开发量;比例计费、发票展示由 Stripe 处理。缺点:定制 UI 空间有限。
方式 B:服务端直接改 Subscription(完全可控)
对已有 subscription_id 调用 Update Subscription API,更新 items 为新的 price,并可设置:
proration_behavior:create_prorations(按日比例补差价或抵扣)或none(下个周期再按新价,依业务选择)。billing_cycle_anchor等高级字段按需使用。
升级与降级在 Stripe 侧并无不同 API,都是换绑 Price;区别在你的定价策略与是否立即生效。
无论哪种方式,以 Webhook 收到的事件为准更新数据库,不要只信前端跳转回来的页面。
第五步:取消订阅
取消也有常见两类:
- 立即取消:将订阅设为
cancel_at_period_end: false并执行取消 API,或 Portal 里用户选择立即结束(若你允许)。 - 周期末取消(更常见):
cancel_at_period_end: true,用户付完当前周期,到期不再续费。
Customer Portal 里可让用户自助取消;若自建 UI,则调用 Subscriptions API 更新 cancel_at_period_end 或 cancel()。
同样:最终状态以 Webhook(如 customer.subscription.deleted、customer.subscription.updated)同步到你的库。
第六步:Supabase Edge Function 接收 Webhook
1. 准备密钥与环境变量
在 Stripe Dashboard → Developers → Webhooks → Add endpoint:
- Endpoint URL:部署后的函数公网地址,例如:
https://<project-ref>.supabase.co/functions/v1/stripe-webhook - 选择要监听的事件,订阅业务至少包括:
checkout.session.completed(首次结账成功,可用来初次写入订阅)customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deleted- (可选)
invoice.paid、invoice.payment_failed用于宽限期与欠费处理
创建后复制 Signing secret(whsec_...)。
在 Supabase 中为该函数配置 Secrets(CLI 示例):
supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_xxx STRIPE_SECRET_KEY=sk_test_xxx
2. 函数职责概要
- 只接受 POST;读取原始 body(字符串),用于验签。
- 使用 Stripe 官方库提供的
constructEvent(rawBody, signature, webhookSecret)(或等价方法)校验Stripe-Signature。 - 验签失败返回 400,不要处理业务。
- 根据
event.type分支,用event.data.object更新数据库(例如subscriptions表:status、current_period_end、price_id、cancel_at_period_end等)。 - 用 Customer metadata 或 Checkout metadata 里的
user_id关联到 Supabase 用户表。 - 尽快返回 200;耗时操作可写入队列或由另一函数异步处理(避免 Stripe 反复重试)。
3. Deno / Edge 注意点
- 使用 Stripe 的 REST API 或兼容 Deno 的 SDK版本;以 Supabase Edge Functions 文档 与 Stripe 当前推荐方式为准。
- 务必使用未解析的原始请求体验签;若框架提前
JSON.parse过 body,验签会失败。 STRIPE_SECRET_KEY仅用于必要时回查 Stripe(例如根据customer拉取最新订阅);Webhook 处理本身主要依赖事件 payload。
4. 本地调试 Webhook
使用 Stripe CLI:
stripe listen --forward-to localhost:54321/functions/v1/stripe-webhook
CLI 会打印临时 whsec_...,本地测试时把该 secret 配进函数环境即可。
第七步:数据库建议(与 Supabase Auth 对齐)
可建一张 subscriptions(或扩展 profiles),至少包含:
user_id(UUID,对齐auth.users)stripe_customer_idstripe_subscription_idstatus(active、past_due、canceled等)price_id/product_id(当前档位)current_period_end(时间戳)cancel_at_period_end(布尔)
权限:仅 service role 或 Edge Function 用 Supabase Admin 客户端写入;普通 anon 不得直接改订阅表。
第八步:上线前检查清单
- 生产环境使用 Live 密钥与 Live Webhook endpoint,
whsec与sk_live存在 Secrets中。 - Dashboard 中配置 Customer Portal 域名与 Checkout 允许的返回 URL。
- Webhook 日志中确认无长期 4xx/5xx;对失败事件可用 Stripe 控制台 Resend。
- 测试:新订、升级、降级、周期末取消、支付失败(测试卡)是否都把库表更新正确。
常见问题
| 现象 | 建议 |
|---|---|
| Webhook 验签失败 | 是否用了原始 body;URL 是否指向正确环境;secret 是否与该 endpoint 一致 |
| 用户已付款但库没更新 | 查 Stripe Webhook 投递记录;函数是否超时;是否在验签前就 parse 了 body |
| 升降级后权益不对 | 以 customer.subscription.updated 为准;检查是否更新了正确的 items[].price.id |
| 测试与生产混乱 | Test/Live 的 Key、price_、whsec 严格分开,Secrets 分环境管理 |