# Integrate Stripe Saas

> Guide Stripe Checkout + Webhooks + Customer Portal integration for SaaS on Cloudflare Workers/Pages or Next.js Edge. Use when adding Stripe payments, subscriptions, credit packs, Price IDs, webhook signing, billing portal, sk_test_/sk_live_ secrets, or when Test/Live vs Preview/Production env is confusing. Prefer this over ad-hoc Stripe SDK wiring on Workers.

- Skill: `gaojuzhang/integrate-stripe-saas` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add gaojuzhang/integrate-stripe-saas`
- Raw SKILL.md: https://api.skillmd.com/api/skills/gaojuzhang/integrate-stripe-saas/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: gaojuzhang (https://skillmd.com/u/gaojuzhang)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/gaojuzhang/integrate-stripe-saas

---


# 集成 Stripe（SaaS：Checkout + Webhook + Portal）

基于 Upload‑Ready 实战沉淀的**可复用流程**。目标：在新站点快速完成「可收款、可续费、可取消」的 Stripe 接入，并避开 Edge/Cloudflare 常见坑。

## 何时使用

- 用户要给站点接 Stripe（订阅、一次性买点、积分包等）
- 部署在 **Cloudflare Pages / Workers / next-on-pages Edge**
- 搞不清 **Stripe Test/Live** 与 **Cloudflare Preview/Production**
- Checkout 502、密钥形态不对、Webhook 验签失败、Pro 无法改订阅

## 执行原则

1. **先决策清单，再改代码**；每步完成后汇报，等用户 Review 再下一步
2. **最小改动**；Dashboard 操作由用户完成，Agent 给逐步点击路径与应粘贴的值名
3. 产品 SKU / 金额 / 权益文案有歧义时 **必须让用户决策**，勿默认拍板
4. 用中文沟通；密钥 **永不** 写入 Git / 聊天复述全文

## 参考实现（可选对照）

若当前仓库附近有 Upload‑Ready，可对照（勿整文件复制业务规则）：

| 能力 | 参考路径 |
|------|----------|
| Checkout（fetch，非 Node SDK） | `apps/web/lib/payment/stripe.ts` |
| 开关 / 密钥形态校验 | `apps/web/lib/payment/paymentFlags.ts` |
| 创建会话 API | `apps/web/app/api/payment/stripe/create-checkout-session/` |
| Portal API | `apps/web/app/api/payment/stripe/create-portal-session/` |
| Webhook | `apps/web/app/api/webhooks/stripe/` |
| 环境说明长文 | `doc/STRIPE.md` |

详细踩坑与自检见 [reference.md](reference.md)。

---

## 决策清单（开干前问清）

```
- [ ] 卖什么：一次性商品？订阅？两者都有？各几个 SKU？
- [ ] 币种（默认 USD）与标价表（权威来源：constants / 设计文档）
- [ ] 宿主：Cloudflare Pages+Edge / 纯 Workers / Node 服务器？
- [ ] 用户体系：登录后才能买？（Checkout metadata 是否要 userId）
- [ ] 订阅变更：是否要 Customer Portal（改卡/取消）？
- [ ] 练手阶段：正式域名是否先用 Stripe Test？（推荐：是）
- [ ] Cloudflare 变量策略：Secret 走 Dashboard？明文 Price 走 wrangler？
- [ ] Webhook URL 用哪个域名？（正式站 / Preview 是否各建 endpoint）
```

**环境矩阵（必读，极易混）：**

| 概念 | 属于谁 | 含义 |
|------|--------|------|
| Test / Live | Stripe 左上角 | 密钥与 Price 是否真扣款 |
| Preview / Production | Cloudflare 部署环境 | **哪次部署**能读到变量 |

**结论：** Production ≠ 必须 Live。正式域名可先挂 `sk_test_` 联调。Test 的 `price_` **绝不能**配 `sk_live_`（反之亦然）。

---

## 进度清单

```
Task Progress:
- [ ] 步骤 0：勘察仓库与 SKU 表
- [ ] 步骤 1：Stripe Dashboard（密钥 + Products/Prices）
- [ ] 步骤 2：部署环境变量（Secret + Price + 开关）
- [ ] 步骤 3：Webhook endpoint + Signing secret
- [ ] 步骤 4：数据模型（customer / subscription / 幂等支付记录）
- [ ] 步骤 5：服务端 API（Checkout + 可选 Portal）
- [ ] 步骤 6：Webhook 处理（权益落库）
- [ ] 步骤 7：前端 CTA + 验收
- [ ] 步骤 8：（可选）切 Live 上线收款
```

---

## 步骤 0：勘察仓库与 SKU 表

确认并输出一张 **SKU 表**（与用户确认后再建 Stripe 商品）：

| # | 类型 | 内部 id | 金额 | 周期 | 环境变量名建议 |
|---|------|---------|------|------|----------------|
| … | one_time / recurring | pack/planId | $x.xx | — / month / year | `STRIPE_PRICE_ID_…` |

同时确认：

- 是否已有用户表、积分/权益字段
- 是否 Edge runtime（有则 **禁止**依赖默认 Node Stripe SDK 做 Checkout）
- `wrangler.toml` / Pages 是否「明文只能写 toml、Secret 只能 Dashboard」

---

## 步骤 1：Stripe Dashboard（用户操作，Agent 引导）

全程先 **Test mode**。

### 1.1 Secret Key

1. [Dashboard](https://dashboard.stripe.com/) → 左上角 **Test mode**
2. **Developers → API keys** → 复制 **Secret key**（必须 `sk_test_`）
3. 写入部署平台 **Secret**（如 `STRIPE_SECRET_KEY`），勿进 Git

> Checkout 跳转模式通常 **不需要** 前端 `pk_`。

### 1.2 创建 Product / Price

对 SKU 表逐行 **Add product**：

- 一次性 → Pricing Type **One-off**
- 订阅 → **Recurring** + Monthly / Yearly
- 币种与标价表一致；复制的是 **Price ID**（`price_…`），不是 `prod_…`
- Name/Description 给顾客看；**禁止**写 `price_`、环境变量名、内部 id

### 1.3（可选）对账单描述符

多产品共账户时：主描述符用**公司商号**；单产品可用 `statement_descriptor_suffix` 区分。细节见 [reference.md](reference.md)。

---

## 步骤 2：部署环境变量

**推荐拆分：**

| 变量 | 存放 | 示例 |
|------|------|------|
| `STRIPE_SECRET_KEY` | Secret | `sk_test_…` / `sk_live_…` |
| `STRIPE_WEBHOOK_SECRET` | Secret | `whsec_…` |
| `STRIPE_PRICE_ID_*` | 明文配置（如 `wrangler.toml` `[env.*.vars]`） | `price_…` |
| `PAYMENT_STRIPE_ENABLED` | 明文 | `"true"` / `"false"` |

Cloudflare Pages 常见约束：

- Dashboard **只能**加 Secret 时：Price / 开关必须进 `wrangler.toml` 的 **每个** `[env.preview.vars]` / `[env.production.vars]`（顶层 `[vars]` **不会**自动合并进 env 段）
- 本地：`.dev.vars` 覆盖密钥（勿提交）

改 toml 后必须 **重新部署** 才生效。

---

## 步骤 3：Webhook

1. Test mode → **Developers → Webhooks → Add endpoint**
2. URL：`https://<域名>/api/webhooks/stripe`（路径按项目约定）
3. 至少订阅：
   - `checkout.session.completed`
   - `invoice.paid`
   - `customer.subscription.updated`
   - `customer.subscription.deleted`
4. 复制 **Signing secret**（`whsec_…`）→ 对应环境的 Secret
5. Endpoint 所属 mode（Test/Live）必须与该环境的 `sk_` / `price_` **同属一套**

---

## 步骤 4：数据模型

最少需要（名称可按项目调整）：

- 用户：`stripe_customer_id`、`stripe_subscription_id`、权益字段（如 `plan`、credits）
- 支付流水：以 `checkout.session.id` / `event.id` / `invoice.id` 做 **幂等**，防重复加权益

已有库则写 migration；执行一次远程迁移后再测支付。

---

## 步骤 5：服务端 API（硬约束）

### 5.1 创建 Checkout Session

- 需登录则校验 session；`metadata` 写入 `userId` + `pack` 或 `planId`
- `mode`：`payment`（一次性）/ `subscription`
- `success_url` / `cancel_url` 指回本站
- **Edge / Workers：用 Stripe HTTP API（`fetch`）创建 Session**，不要用会拖垮 isolate 的 Node SDK 默认路径
- 启动前校验密钥形态：仅接受 `sk_test_` / `sk_live_`（或项目约定的 `rk_`）；`pk_` / `whsec_` / 乱码 → **503 JSON**，禁止抛到 Worker 崩溃

### 5.2 Customer Portal（订阅管理）

- `POST` 创建 portal session，需已有 `stripe_customer_id`
- 返回 URL 后浏览器跳转；取消/换卡仍靠 **同一套 Webhook** 回写权益

### 5.3 业务互斥（按产品）

例：已有有效订阅时拒绝再开订阅 Checkout；积分包是否叠加由产品决定。

---

## 步骤 6：Webhook 处理

1. `export const runtime = "edge"`（若项目要求 Edge）
2. **`const rawBody = await req.text()`** 后再验签；**禁止**先 `req.json()`
3. 校验 `stripe-signature`；失败 → 400，不落库
4. 按事件更新权益；**幂等**
5. 尽快 `200`；重逻辑保持短（Workers CPU 限制）

验签可用官方 SDK 的 async construct，或等价 HMAC；Checkout 创建仍优先 `fetch`。

---

## 步骤 7：前端 + 验收

- 定价页 CTA → 调 create-checkout-session → `window.location = url`
- 已订阅 → 「管理订阅」走 Portal
- Test 卡：`4242 4242 4242 4242`，任意未来有效期 / CVC

**验收清单：**

```
- [ ] Secret 前缀正确，且与 Price 同属 Test 或同属 Live
- [ ] 五个（或 N 个）变量都是 price_ 不是 prod_
- [ ] 开关已开；Webhook whsec_ 对应该 endpoint
- [ ] 一次成功支付后权益正确；重复 Webhook 不加两次
- [ ] 订阅取消 / 更新后 plan 正确
- [ ] Portal 在有 customer id 时可打开
```

---

## 步骤 8：切 Live（可选）

左上角切 **Live mode**，**整套重做**：Live `sk_`、Live `price_`、Live Webhook `whsec_`，只改 **Production** 环境。Test/Live **禁止混用**。

---

## 常见失败速查

| 现象 | 优先检查 |
|------|----------|
| Cloudflare HTML 502 / isolate 崩 | 是否在 Edge 用了 Node Stripe SDK 建 Checkout → 改 `fetch` |
| Checkout 503 / keyPrefix 怪异 | Secret 不是 `sk_test_`/`sk_live_`（粘错 pk/whsec/截断） |
| 支付成功但无权益 | Webhook URL/环境、`whsec_`、是否用了 `req.json()` 验签 |
| Portal 失败 | 用户无 `stripe_customer_id`（从未成功订阅过） |
| Preview 无 Stripe 变量 | `wrangler.toml` 只写了顶层 `[vars]`，未写入 `[env.preview.vars]` |

更多见 [reference.md](reference.md)。

