
InsForge Stripe 支付集成指南Checkout、订阅、Billing Portal 与 Webhook 履约全流程【免费下载链接】InsForgeThe all-in-one, open-source backend platform for agentic coding. InsForge gives your coding agent database, auth, storage, compute, hosting, and AI gateway to ship full-stack apps end-to-end.项目地址: https://gitcode.com/GitHub_Trending/in/InsForge导读本文是基于 InsForge 开源仓库中的 Stripe 支付 Agent 文档 整理并深化的技术指南面向在 InsForge 后端平台数据库、认证、存储、AI Gateway 等一站式后端上为 Agent 应用接入 Stripe 支付的开发者。读完本文你将掌握如何用 InsForge 的 Stripe Payments API 完成一次性 Checkout、订阅 Checkout、客户 Billing Portal、基于 Webhook 的订单履约/权益发放以及配置调试与安全加固的完整实战方案。一、InsForge Stripe 支付能做什么InsForge 将 Stripe 集成封装为平台级能力官方文档明确建议在以下场景使用 StripeStripe Checkout 一次性支付one-time paymentsStripe Checkout 订阅subscriptionsStripe Billing Portal 链接为已有客户提供订阅管理入口Stripe Products 与 Prices商品与价格目录同步Stripe 托管的订阅与发票生命周期订阅、续费、账单、发票由 Stripe 全权管理。与之对应的边界约束是不要自建卡片采集 UI也不要在一个 Stripe 流程中混用 Razorpay 的 Orders、Items、Plans 概念——InsForge 将 Stripe 与 Razorpay 视为两套独立的支付抽象支付入口路由 按 provider 分别暴露能力。二、接入前必须确认的 5 项前提官方 Agent 文档给出了“Before Coding”清单这是把 Stripe 接入 InsForge 前必须逐项确认的硬性条件默认使用environment: test除非用户明确批准修改 live 环境确认目标环境的 Stripe Secret Key 已配置确认该环境下 Product ID 与 Price ID 真实存在确认后端能收到 Stripe Webhook——当后端可访问时InsForge 会自动托管 Stripe Webhook 端点将 Checkout successUrl 仅视为 UX 跳转真正的履约fulfillment必须来自 Webhook。第 4 点与第 5 点是整套方案可靠性的关键successUrl 只是用户在 Stripe 完成支付后回到你应用页面的重定向地址它可能在 Webhook 事件处理完成前就被访问因此绝不能依赖它来判断支付结果。2.1 项目管理员如何配置 Stripe项目管理员可通过Dashboard → Payments → Settings完成配置也可使用 CLI 命令# 查看当前 Stripe 配置状态 npx insforge/cli payments stripe status # 为指定环境设置 Stripe 密钥test 或 live npx insforge/cli payments stripe config set --environment test sk_test_xxx # 同步商品/价格目录到 InsForge npx insforge/cli payments stripe sync --environment test # 配置 Stripe Webhook 端点 npx insforge/cli payments stripe webhooks configure --environment test2.2 密钥存储在源码中的对应实现从源码看Stripe 密钥并不是硬编码在代码里的而是按环境映射到固定的 Secret 名称再由 StripeConfigService 通过SecretService读取constants.ts 定义了映射关系const STRIPE_SECRET_KEY_BY_ENVIRONMENT: RecordStripeEnvironment, string { test: STRIPE_TEST_SECRET_KEY, live: STRIPE_LIVE_SECRET_KEY, }; const STRIPE_WEBHOOK_SECRET_BY_ENVIRONMENT: RecordStripeEnvironment, string { test: STRIPE_TEST_WEBHOOK_SECRET, live: STRIPE_LIVE_WEBHOOK_SECRET, };也就是说除了通过 CLI 设置也可以在启动环境中注入STRIPE_TEST_SECRET_KEY/STRIPE_LIVE_SECRET_KEY环境变量seedStripeKeysFromEnv会在启动阶段自动完成初始化见 config.service.ts。密钥写入前会调用validateStripeSecretKey校验格式并经由EncryptionManager.encrypt加密后存入system.secrets表更换密钥时若检测到 Stripe 账户 ID 变化还会自动清理该环境下的旧支付数据并重建托管 Webhookconfig.service.ts。三、运行时初始化与鉴权模型3.1 初始化 InsForge 客户端在应用代码中使用 TypeScript SDK 初始化import { createClient } from insforge/sdk; const insforge createClient({ baseUrl: https://your-project.insforge.app, anonKey: your-anon-key });3.2 用户上下文为什么 API Key 不能替代用户 TokenCheckout 需要一个 InsForge 用户 Token。访客的一次性 Checkout 可以使用匿名 InsForge Token但 API Key 不能替代因为后端需要用户上下文才能写payments.stripe_checkout_sessions表。这一约束在源码中有直接体现StripeCheckoutService的插入角色白名单只允许anon、authenticated、project_admin三种角色其他角色一律返回 403checkout.service.ts 与 checkout.service.ts写入时还会带上当前用户上下文withUserContext确保 RLS 策略能基于auth.uid()生效。而 Customer Portal 会话创建要求更严格——必须为已认证用户匿名角色直接返回 401customer-portal.service.ts。四、一次性支付Checkout Session 实战4.1 标准流程先建应用侧订单再发起 Checkout官方文档给出的完整示例是先在应用自有表如orders写入一条 pending 订单再调用createCheckoutSession创建 Stripe Checkout 会话并把订单 ID 放进metadata与idempotencyKey最后将用户重定向到data.checkoutSession.urlconst { data: order, error: orderError } await insforge .from(orders) .insert([{ user_id: user.id, status: pending }]) .select() .single(); if (orderError) throw orderError; const { data, error } await insforge.payments.stripe.createCheckoutSession(test, { mode: payment, lineItems: [{ priceId: price_123, quantity: 1 }], successUrl: ${window.location.origin}/orders/${order.id}, cancelUrl: ${window.location.origin}/pricing, customerEmail: user.email, metadata: { order_id: order.id }, idempotencyKey: order:${order.id} }); if (error) throw error; if (data?.checkoutSession.url) { window.location.assign(data.checkoutSession.url); }匿名一次性购买省略subject并在有邮箱时传入customerEmail即可。4.2 幂等性在源码中的三层保障重复发起 Checkout 是常见事故InsForge 在服务端对幂等性做了三层防护checkout.service.ts 与 checkout.service.ts数据库唯一约束payments.stripe_checkout_sessions表对(environment, idempotency_key)有部分唯一索引重复的 key 直接DO NOTHING随后回查已有会话并复用请求指纹比对服务端对请求做 stable JSON 序列化后计算 SHA-256request_hash只有指纹一致才返回已有会话指纹不一致会返回409 PAYMENT_CHECKOUT_ALREADY_EXISTScheckout.service.tsAdvisory Lock同环境下按payments_checkout_{environment}_{idempotencyKey}加会话级咨询锁避免并发竞态环境级还有payments_environment_{environment}共享锁checkout.service.ts。传给 Stripe 的幂等键最终会被组装成insforge:{environment}:checkout_session:{callerKey}的命名空间格式helpers.ts。4.3 metadata 的保留规则传入的metadata会原样透传给 Stripe但有两个系统保留键由平台注入应用不得占用insforge_前缀checkout.service.tsinsforge_checkout_mode记录payment/subscription模式insforge_checkout_session_id记录 InsForge 侧 Checkout 会话 IDclientReferenceId也使用该值若应用传入subject还会写入insforge_subject_type与insforge_subject_id见 helpers.ts。如果应用 metadata 中出现insforge_开头的 key服务端会直接抛 400 拒绝请求。五、订阅支付需要 Billing Subject订阅与一次性支付的关键差异在于订阅必须提供一个 billing subject。文档要求选择一个稳定的应用所有者维度——user、team、organization、workspace、tenant 或 group 均可。const { data, error } await insforge.payments.stripe.createCheckoutSession(test, { mode: subscription, subject: { type: team, id: teamId }, lineItems: [{ priceId: price_monthly_123, quantity: 1 }], successUrl: ${window.location.origin}/billing/success, cancelUrl: ${window.location.origin}/billing, customerEmail: user.email, idempotencyKey: team:${teamId}:pro-monthly }); if (error) throw error; if (data?.checkoutSession.url) { window.location.assign(data.checkoutSession.url); }对应地服务端在mode subscription且缺少subject时会直接抛 400「Subscription checkout requires a billing subject」checkout.service.ts。安全红线不要让用户随意提交任意的subject.type/subject.id除非应用自己校验了该用户有权管理这个 billing subject团队归属校验应放在应用层或依赖 RLS。六、Customer PortalBilling Portal 会话当 Checkout 已经为 subject 创建了客户映射后就可以让用户进入 Stripe Billing Portal 管理订阅、发票和支付方式const { data, error } await insforge.payments.stripe.createCustomerPortalSession(test, { subject: { type: team, id: teamId }, returnUrl: ${window.location.origin}/billing }); if (error) { if (statusCode in error error.statusCode 404) { return; } throw error; } if (data?.customerPortalSession.url) { window.location.assign(data.customerPortalSession.url); }Portal 创建有两个前置条件已认证用户payments.customer_mappings表中已存在该 subject 的 Stripe customer 映射。当映射不存在时服务端返回 404对应PAYMENT_NOT_FOUND前端代码应捕获 404 静默返回customer-portal.service.ts。映射关系是在checkout.session.completed等 Webhook 事件中被写入的webhook.service.ts因此“先 Checkout、后 Portal”的顺序不可颠倒。七、履约Fulfillment基于 Webhook 事件触发器7.1 核心原则履约必须由 Webhook 驱动且必须做到幂等与可降级。官方文档明确强调两点InsForge 会在将事件标记为processed之前以事务方式提交该事件派生的所有行但跨事件不保证顺序——Stripe 可能先投递invoice.paid再投递checkout.session.completed因此你的触发器触发时另一事件创建的payments.customer_mappings行可能还不存在。正确姿势是先从事件 payload 解析 billing subject再用payments.customer_mappings作为兜底解析不出 subject 时绝不能静默跳过履约而要写日志或进入死信处理。InsForge 服务端侧会先在payments.webhook_events表记录事件起始状态recordWebhookEventStart处理成功后标记processed失败标记failed未知类型标记ignoredwebhook.service.ts。订阅、支付、退款等 20 个事件类型由平台托管处理见 constants.ts而应用自己的履约逻辑应通过监听payments.webhook_events表的触发器来实现。7.2 一次性支付履约更新订单状态以下触发器在 Stripecheckout.session.completed事件被处理后将orders表中对应的 pending 订单更新为 paidCREATE OR REPLACE FUNCTION public.fulfill_paid_order() RETURNS TRIGGER AS $$ BEGIN IF NEW.provider stripe AND NEW.event_type checkout.session.completed AND NEW.processing_status processed AND (NEW.payload - data - object - metadata - order_id) IS NOT NULL THEN UPDATE public.orders SET status paid, paid_at COALESCE(NEW.processed_at, NOW()) WHERE id::text NEW.payload - data - object - metadata - order_id AND status pending; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql SECURITY DEFINER; CREATE TRIGGER fulfill_paid_order_from_stripe_webhook AFTER INSERT OR UPDATE ON payments.webhook_events FOR EACH ROW EXECUTE FUNCTION public.fulfill_paid_order();要点同时监听INSERT与UPDATE因为事件先插入、后更新为processed用WHERE status pending保证幂等SECURITY DEFINER让触发器以定义者权限写应用表。7.3 订阅履约解析 billing subject 并发放权益订阅事件如invoice.paid不携带应用自定义 metadata。InsForge 在 Checkout 时把insforge_subject_type/insforge_subject_id印到订阅 metadata 上Stripe 会将其快照到订阅生成的发票上parent.subscription_details.metadata。因此解析顺序应为parent.subscription_details.metadata→invoice.metadata→payments.customer_mappings兜底这也是 InsForge 内部使用的顺序CREATE OR REPLACE FUNCTION public.grant_subscription_access() RETURNS TRIGGER AS $$ DECLARE v_subject_type TEXT; v_subject_id TEXT; BEGIN IF NEW.provider stripe AND NEW.event_type invoice.paid AND NEW.processing_status processed THEN v_subject_type : COALESCE( NEW.payload - data - object - parent - subscription_details - metadata - insforge_subject_type, NEW.payload - data - object - metadata - insforge_subject_type ); v_subject_id : COALESCE( NEW.payload - data - object - parent - subscription_details - metadata - insforge_subject_id, NEW.payload - data - object - metadata - insforge_subject_id ); IF v_subject_id IS NULL THEN SELECT m.subject_type, m.subject_id INTO v_subject_type, v_subject_id FROM payments.customer_mappings m WHERE m.provider NEW.provider AND m.environment NEW.environment AND m.provider_customer_id NEW.payload - data - object - customer; END IF; IF v_subject_id IS NULL THEN RAISE WARNING Stripe event % has no resolvable billing subject, NEW.provider_event_id; RETURN NEW; END IF; -- Branch on the subject type sent at checkout; team_id is a UUID here, -- so the type check also guards the cast. IF v_subject_type team THEN INSERT INTO public.team_entitlements (team_id, plan, active, updated_at) VALUES (v_subject_id::uuid, pro, true, NOW()) ON CONFLICT (team_id) DO UPDATE SET plan EXCLUDED.plan, active true, updated_at NOW(); END IF; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql SECURITY DEFINER; CREATE TRIGGER grant_subscription_access_from_stripe_webhook AFTER INSERT OR UPDATE ON payments.webhook_events FOR EACH ROW EXECUTE FUNCTION public.grant_subscription_access();两个工程细节值得注意v_subject_type team的类型判断同时充当了::uuid强转的安全守卫避免把非 UUID subject 强转引发异常撤销权益按同样思路处理customer.subscription.deleted与customer.subscription.updated事件——订阅事件对象payload - data - object - metadata上同样带有insforge_subject_type/insforge_subject_id键。触发器目标表请替换为应用为对应 billing subject 类型准备的权益表。另外payments.transactions表仅用于 Dashboard 与报表展示不要把它当作履约依据。八、安全加固清单官方文档给出的安全要求逐条落实如下共享 subject团队/组织的 Checkout 或 Portal 流程必须加 RLS 或服务端成员关系校验防止任何登录用户都能替他人发起 Checkout建议对payments.stripe_checkout_sessions与payments.stripe_customer_portal_sessions配置 RLSPostgreSQL 的SELECT策略同样作用于INSERT ... RETURNING返回的行以及幂等重试查询。如果 Checkout 明明有INSERT策略却仍被拒绝请为同一 billing subject 与幂等键补充匹配的SELECT可见性策略。服务端会把 RLS 权限错误PostgreSQL 错误码 42501归一化为 403AUTH_UNAUTHORIZEDcheckout.service.ts不要把以下表直接暴露给终端用户payments.customers、payments.transactions、payments.stripe_subscriptions、payments.stripe_subscription_items不要直接写 Stripe 托管的数据表一律通过 Payments API、Stripe Webhook 或应用自有触发器目标表写入metadata 键以insforge_开头为系统保留应用不得占用。九、调试四组即查即用的 SQL文档提供了四组高价值排障 SQL可直接在 InsForge 数据库上执行。1. 最近的 Checkout 尝试SELECT id, environment, mode, status, payment_status, subject_type, subject_id, checkout_session_id, customer_id, subscription_id, last_error, created_at, updated_at FROM payments.stripe_checkout_sessions ORDER BY created_at DESC LIMIT 20;2. 客户映射customer_mappingsSELECT provider, environment, subject_type, subject_id, provider_customer_id, created_at, updated_at FROM payments.customer_mappings WHERE provider stripe ORDER BY updated_at DESC LIMIT 20;3. Stripe 交易记录SELECT provider, environment, type, status, subject_type, subject_id, provider_object_type, provider_object_id, amount, currency, paid_at, failed_at, refunded_at, created_at FROM payments.transactions WHERE provider stripe ORDER BY created_at DESC LIMIT 20;4. Webhook 失败事件SELECT provider, environment, provider_event_id, event_type, processing_status, attempt_count, last_error, received_at, processed_at FROM payments.webhook_events WHERE provider stripe AND processing_status IN (failed, pending) ORDER BY received_at DESC LIMIT 20;服务端事件处理的核心循环对应 webhook.service.tscheckout.session.*系列更新 Checkout 会话状态并生成 transactioninvoice.paid/invoice.payment_failed写入发票交易payment_intent.*、charge.refunded、refund.*维护支付与退款交易customer.subscription.*系列更新订阅投影表——排查时可结合这些表交叉验证事件链路。十、常见故障速查表症状排查方向Checkout 返回 Stripe key not configured检查是否配置了正确的test或liveStripe 密钥对应STRIPE_TEST_SECRET_KEY/STRIPE_LIVE_SECRET_KEYCheckout 使用了错误的价格确认 Price ID 属于当前选中的环境test 与 live 的商品目录是隔离的同步也按环境进行重复创建 Checkout 会话使用基于订单、购物车或 billing subject 的稳定idempotencyKey服务端会按 key 去重Portal 返回 not foundsubject 还没有 Stripe customer 映射——让客户先完成一次 Checkoutcheckout.session.completed事件会写入payments.customer_mappingsStripe 侧已扣款但 InsForge 无记录检查 Stripe Webhook 配置与payments.webhook_events表确认托管 Webhook 端点{API_BASE_URL}/api/webhooks/stripe/{environment}见 config.service.ts是否可被 Stripe 访问、签名密钥是否就绪用户可以为其他团队发起 Checkout为 billing subject 增加 RLS 或服务端成员关系校验。十一、进一步阅读官方 Agent 文档源文件.agents/docs/payments-stripe.mdRazorpay 对照文档.agents/docs/payments-razorpay.md支付总览.agents/docs/payments.md服务端核心实现checkout.service.ts、customer-portal.service.ts、webhook.service.ts、config.service.ts、constants.tsProvider 层Stripe API 封装与密钥校验stripe.provider.tsAPI 路由与共享 Schemabackend/src/api/routes/payments/stripe/index.routes.ts、backend/src/api/routes/webhooks/stripe.routes.ts、packages/shared-schemas/src/payments.schema.ts面向开发者的 SDK 用法docs/sdks/typescript/payments-stripe.mdx以及通用支付概念文档 docs/core-concepts/payments/stripe.mdx。【免费下载链接】InsForgeThe all-in-one, open-source backend platform for agentic coding. InsForge gives your coding agent database, auth, storage, compute, hosting, and AI gateway to ship full-stack apps end-to-end.项目地址: https://gitcode.com/GitHub_Trending/in/InsForge创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考