配置

各类配置分别放在哪里:src/config 里的配置文件、环境变量、wrangler.jsonc,以及运营人员在管理后台里修改的设置。

ShipKit 的配置分布在四个地方,各管一类问题:

位置放什么由谁、怎么改
src/config/*.ts产品层面的决定:名称、URL、套餐、落地页、主题改代码,然后部署
.dev.vars / Worker secret凭据:认证密钥、OAuth、支付和邮件的 key本地改 .dev.vars,线上用 wrangler secret put
wrangler.jsoncCloudflare 资源:Worker 名称、D1、R2、限流、定时任务改文件,然后部署
管理后台的“设置”运营参数:保留天数、宽限期、各种上限运营人员在运行时修改,无需部署

密钥放环境变量;需要让运营人员不找开发就能调的值,做成运营设置;其余的都写在代码里。

src/config 里的配置文件

文件内容
app-config.ts应用名称、简介、规范 URL、邮件地址、法律主体、Google client id、分析 key、候补名单开关、媒体 URL
plans.ts订阅套餐和积分包:价格、每月发放的积分、Stripe price id 和 Creem product id
landing.ts以区块数组表示的落地页,见落地页和主题
themes.ts四套视觉主题,以及访客首次看到的 defaultTheme
private-paths.ts不属于营销页的顶级路径。这一份列表同时供预渲染过滤和 robots.txt 的 Disallow 行使用

组件直接 import 这些文件,而不是去读 import.meta.env。大部分面向用户的文案不在这里,而在messages/en.json 和 messages/zh.json 里(见多语言);应用名称以参数的形式传进这些文案,所以改界面上的应用名只需要改一处。

app-config.ts

里面的每一项都是占位值,第一次部署前要逐项过一遍。

字段用途
name、description界面、邮件和页面标题里的产品名;营销页页脚的一句话简介
url线上的规范域名:canonical 和 hreflang 链接、社交分享卡片的地址、邮件里的链接
emailFrom事务邮件的发件人。放在一个已经在 Resend 验证过的发信子域名上,比如 noreply@send.yourdomain.com
supportEmail每封邮件的 Reply-To、“帮助与反馈”里的客服链接、营销页页脚、法律页面。必须能收到信
googleClientIdGoogle OAuth client id,供浏览器里的 One Tap 使用。必须和 GOOGLE_CLIENT_ID 是同一个客户端;文件里开发和线上各有一个值
legalEntity公司名称、司法辖区、地址和 DMCA 联系方式,会印在服务条款、隐私政策和 DMCA 页面上
analytics.posthogPostHog 项目 key 和所在集群的地址。key 留空即关闭分析
waitlist为 true 时,新用户会先进入 /waitlist,等管理员批准。这是构建期开关,预渲染的页面才能显示对应的按钮
mediaUrl营销图片和视频的公开源站。留空时显示占位色块,而不是裂图

url 和环境变量 APP_URL 存的都是你的域名,但分工不同:url 编译进页面,APP_URL 供服务端生成认证回调和结账返回地址。线上两者都应该是你的正式域名。

还有几个品牌相关的文件不在 src/config 里:public/manifest.json、public/ 下的图标,以及社交分享卡片 public/og.png,它由 bun run og 根据 scripts/og-card.html 重新生成。

plans.ts

价格的唯一来源:webhook 会拿收到的 Stripe price id 或 Creem product id 到这个文件里查找,没列在这里的 id 就认不出来。id 出厂时都是 REPLACE_ME 占位符,由 /stripe 或 /creem skill 写入真实值。详见支付和积分。

环境变量

本地从 .dev.vars 读取密钥,线上从 Worker secret 读取。.dev.vars.example 是完整列表,src/core/env.ts 用 zod 做校验。必填项在第一次使用时检查,缺了就让请求直接报错并写明是哪个键;可选项只在用到它的代码里检查,所以删掉一个功能永远不会让应用启动失败。空字符串视为未设置。

变量必填作用
APP_URL是应用的完整源地址,本地是 http://localhost:3000
BETTER_AUTH_SECRET是给会话 cookie 签名,并加密 secret 类型的运营设置。至少 16 个字符,用 openssl rand -base64 32 生成
GOOGLE_CLIENT_ID是Google OAuth client id
GOOGLE_CLIENT_SECRET是Google OAuth client secret
GITHUB_CLIENT_ID否和对应的 secret 一起设置后,登录页会出现“使用 GitHub 继续”
GITHUB_CLIENT_SECRET否两个 GitHub 键要么都设,要么都不设
PAYMENT_PROVIDER否stripe 或 creem,决定新订单走哪一家。留空时:设置了 STRIPE_SECRET_KEY 就用 Stripe,否则用 Creem
STRIPE_SECRET_KEY否Stripe 的 secret API key
STRIPE_WEBHOOK_SECRET否校验 /api/webhooks/stripe。本地由 stripe listen 给出
CREEM_API_KEY否Creem API key
CREEM_WEBHOOK_SECRET否校验 /api/webhooks/creem
RESEND_API_KEY否通过 Resend 发邮件。没有它时每次发送都只记一条日志,登录验证码打印在 dev 日志里
DEMO_MODE否设为 true 会把这个部署变成公开演示站。正式部署上绝对不要设置

购买按钮会检查当前服务商的 key:在它设置之前,按钮显示“即将开放”,不会发起结账,所以在接好支付之前应用也能正常使用。

环境变量在启动时读取,改完 .dev.vars 要重启 dev server。更换 BETTER_AUTH_SECRET 会让所有人掉线,并让已保存的 secret 类型设置无法解密,所以上线之后就把它当作不可更改的值。怎么把密钥推到线上,见部署。

新增变量时,要同时加到 .dev.vars.example 和 src/core/env.ts 的 schema 里。D1 和 R2 不需要任何变量:它们是 binding。

wrangler.jsonc

应用在 Cloudflare 这一侧的配置。最常改的几个字段:

字段默认值说明
nameshipkitWorker 的名称,也是它的 workers.dev 子域名
d1_databases[0].database_idREPLACE_ME创建线上数据库时填入。本地开发不受影响
d1_databases[0].database_nameshipkit必须和你创建的数据库同名
r2_buckets[0].bucket_nameshipkit-files私有文件存储
ratelimits每分钟 5 次和 200 次PUBLIC_RATE_LIMIT 用于公开表单,ANALYTICS_RATE_LIMIT 用于分析代理
triggers.crons0 0 * * *每天运行一次,执行所有注册的定时任务

binding 的名称(DB、BUCKET 和两个限流器的名称)不要改,代码里直接引用它们。改完这个文件后,运行 bun run cf-typegen 重新生成 Env 类型。

运营设置

有些值属于经营业务的人,而不属于代码库:审计日志保留多久、漏掉一次 webhook 后订阅还能撑几天。这些值放在 /admin/settings(后台“设置 → 系统设置”),改了不需要部署。

设置默认值控制什么
已删除账号保留期30 天已关闭的账号在被彻底抹除之前保留多久,期间可以恢复
订阅宽限期3 天周期结束后没收到续费事件,订阅还能维持多久
低余额提示线50 积分低于这个值时,顶栏的余额标签变成充值提示
Webhook 数据保留期90 天已处理的支付 webhook 保留多久
审计日志保留期180 天更早的记录每晚清理(最少 7 天)
邮件日志保留期90 天更早的投递记录每晚清理
佣金比例2000 bps(20%)推荐订单的推广佣金,只对新订单生效
退款冻结期30 天佣金冻结多久之后才能提现
最低提现额5000 美分推广者申请提现所需的可用余额
归因有效期30 天一次推荐点击在多长时间内有效
每条反馈的截图数3一条反馈最多附几张图
截图大小上限5 MB每张图

这些设置的行为:

  • **默认值在代码里。**数据库只存被改过的值。没人动过的键跟随功能代码里的默认值,所以你在代码里改了默认值,所有没覆盖过它的部署都会生效。每一行都可以重置回默认值。
  • **修改最多一分钟生效。**每个 Worker isolate 会把值缓存 60 秒。
  • **范围有约束。**每个数值设置都有最小值和最大值,表单里检查一次,服务端再检查一次。
  • **受权限控制。**查看需要 settings.read,修改需要 settings.write,见权限。
  • **删掉功能,它的设置也一起消失。**每一组设置都属于某个功能,删掉功能时随之移除。

新增一个设置

功能在自己的 settings.ts 里用 registerSetting() 声明设置:带命名空间的键(比如billing.grace_days)、类型(number、boolean、string 或 secret)、代码里的默认值、可选的范围和单位,以及以 message 函数形式提供的标签和说明,这样后台能按语言显示。然后在src/core/settings/handlers.ts 里加一行 import。src/features/billing/settings.ts 很短,照着抄就行。

服务端用 src/core/settings/store.ts 里的 getNumberSetting('billing.grace_days')(或者getSetting、getSecretSetting)读取值。secret 类型加密存储,永远不会返回给浏览器;标记为visibility: 'public' 的设置任何已登录用户都能读取,用于浏览器端需要执行的限制。

有一条约束:settings.ts 也会被打进浏览器的 bundle,所以它不能 import 任何只在服务端可用的东西。默认值放在旁边一个客户端安全的 config.ts 里,billing 就是这么做的。写错了src/core/settings/handlers.test.ts 会失败。新功能还能接入哪些注册表,见添加功能。