我翻了翻一年前写的工时管理应用的提交历史。第一个提交在 15:48。到 16:55,也就是六十七分钟后,第二个有意义的提交叫 "remove Stripe API routes for checkout, portal and webhook"。三个文件出局:68 行 checkout、48 行 portal、222 行 webhook。那时候我连主界面都还没写。
这不是一时冲动。这就是那个模式:克隆脚手架,第一件事是关掉支付。
那个条件反射:克隆,然后删
我用的脚手架出厂就装好了 Stripe。带上它是好事:真需要的时候,现成的能省一天。问题是它默认是开着的,而开着就意味着它拖着一堆东西。
在今年春天起的一个内部模板里,"remove Stripe integration and related billing features" 是仓库的第二个提交,和 init project 同一天。这个 diff 不只动了路由。它顺手带走了:
- 整张
user_billing表。customer_id、price_id、sub_credits、otp_credits、initial_sub_date、last_otp_date、has_sub、cancel_next_month。九列加一个user_id上的索引,管理一笔并不存在的收款。 - 配置里硬编码的
priceId。 两个示例套餐,带着从别的项目复制来的真实 Stripe 标识(price_1R3Rhw...)。这是定时炸弹:没人会删它,但总有人会部署它。 - 五个环境变量。
USE_STRIPE、NEXT_PUBLIC_USE_STRIPE、STRIPE_PUBLIC_KEY、STRIPE_SECRET_KEY、STRIPE_WEBHOOK_SECRET。.env.example里多五条,总有人要去琢磨它们是不是必需的。 - 第一个迁移。 删掉重新生成了,因为 billing 表在里面。
还有 ConfigFile,它本来声明的是 [key: string]: string | string[] | PricingConfig | PricingConfig[],现在变成了 string | string[]。一个不再需要考虑定价套餐的类型,在接下来三个月里用起来舒服得多。
为什么支付是最不能告诉我事情的那部分
Stripe 的 webhook 不是多一条路由而已。那是 222 行代码,要验签名,要区分 checkout.session.completed、invoice.paid 和 customer.subscription.deleted,还要从你的会话之外往数据库写东西。这东西得用 CLI 转发事件来测,得做成幂等的,还得决定同一个事件来两次会怎样。
这些工作都是真实的。只不过它回答的是一个我还没资格问的问题:我该收多少钱?而这个问题排在另一个便宜得多的问题后面:这东西对谁有用吗?
在我不知道第二件事之前,webhook 就是我维护但从不运行的代码。等哪天真需要了,我一个下午就能把它放回去,而且这次是带着已经想清楚的定价模型,而不是两个继承来的示例套餐。
第一轮修剪还会砍掉什么
Stripe 不会一个人走。在一个三天做完的故障管理 MVP 里,仓库的第二个提交名字就叫 "remove useless things"。在第一个提交之后两小时十一分钟。它带走了整个后台面板(250 行的 client-page.tsx、380 行的用户表格、114 行的图表)、feedback 路由、feedback 表和它的 schema。Stripe 的路由没删:改名成了 .txt。route.ts 变成 route.txt。它们不再编译,对 Next 来说不再存在,但万一哪天要用还在那儿。
第二天,同一个项目里,magic link 也倒了。认证页面有两条路径:Google,和带验证码的邮箱,配着它的 nonce、magicLinkSent、magicLinkCode、handleMagicLinkCode。那个提交叫 "eliminar magic link (solo Google)",从页面里带走 116 行。在一个所有人都有 Google 账号的公司内部应用里,第二条路径只是多了一堆要维护的状态。
在另一个模板里,几个月前,同样的刀落在后台上:/admin 出局,/account 出局,hasAdminRole() 出局,.env.example 里的 ADMIN_EMAILS 出局。还有用户 schema 里只为面板服务的四列:role、banned、ban_reason、ban_expires。外加 sessions 表里的 impersonated_by。
最后这个是我最喜欢的例子。sessions 里的 impersonated_by 字段不是便利功能:它是一个安全决策,谁要动认证就得先理解它。在一个没有后台面板的项目里"以防万一"留着它,等于装了一扇上着锁、但四周没有墙的门。
哪些情况下我确实留着它
这不是一条反对收费的规则。这是一条关于时机的规则。
在一个图像生成的 SaaS 里,Stripe 很早就进来并留下了:checkout 启用,六天后 webhook 上线、每日额度开始运转。在那里收费不是商业假设,而是成本控制:有人按下按钮的那一刻,每一次生成都在花真金白银。没有额度就没有产品,只有一张账单。
一个转录 API 也一样:计费和免费套餐的限额是在用户账号体系还在搭的时候就写好的,因为处理的每一分钟音频从第一个请求起就是有价格的。
把两边并排放,判断就很清楚:如果每一次使用都要花钱,收费就是基础设施,从第一天就在。如果边际成本是零,收费就是产品,等你知道自己在卖什么再说。 一个工时工具、一个内部故障管理系统或一个模板,每个用户不花一分钱。一个图像模型会。
修剪之后剩下什么
第一个小时过去后,所有项目里活下来的东西总是一样:单一提供方的认证、只有领域表的数据库、以及应用做正事所需的路由。这就是我所说的基础设施。其他一切都是对产品未来的押注,而押注即使输了也要拿维护成本去付。
一个完整的模板给你省下第一个下午。删掉用不上的部分,给你省下接下来三个月。
