配置
各类配置分别放在哪里:src/config 里的配置文件、环境变量、wrangler.jsonc,以及运营人员在管理后台里修改的设置。
ShipKit 的配置分布在四个地方,各管一类问题:
| 位置 | 放什么 | 由谁、怎么改 |
|---|---|---|
src/config/*.ts | 产品层面的决定:名称、URL、套餐、落地页、主题 | 改代码,然后部署 |
.dev.vars / Worker secret | 凭据:认证密钥、OAuth、支付和邮件的 key | 本地改 .dev.vars,线上用 wrangler secret put |
wrangler.jsonc | Cloudflare 资源: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、“帮助与反馈”里的客服链接、营销页页脚、法律页面。必须能收到信 |
googleClientId | Google OAuth client id,供浏览器里的 One Tap 使用。必须和 GOOGLE_CLIENT_ID 是同一个客户端;文件里开发和线上各有一个值 |
legalEntity | 公司名称、司法辖区、地址和 DMCA 联系方式,会印在服务条款、隐私政策和 DMCA 页面上 |
analytics.posthog | PostHog 项目 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 这一侧的配置。最常改的几个字段:
| 字段 | 默认值 | 说明 |
|---|---|---|
name | shipkit | Worker 的名称,也是它的 workers.dev 子域名 |
d1_databases[0].database_id | REPLACE_ME | 创建线上数据库时填入。本地开发不受影响 |
d1_databases[0].database_name | shipkit | 必须和你创建的数据库同名 |
r2_buckets[0].bucket_name | shipkit-files | 私有文件存储 |
ratelimits | 每分钟 5 次和 200 次 | PUBLIC_RATE_LIMIT 用于公开表单,ANALYTICS_RATE_LIMIT 用于分析代理 |
triggers.crons | 0 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 会失败。新功能还能接入哪些注册表,见添加功能。