cmux-billing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

cmux Billing

cmux账单

Read before changing billing, pricing, Stripe, Pro entitlement, checkout, webhook, or subscription code.
在修改账单、定价、Stripe、专业版权限、结账、webhook或订阅相关代码前,请先阅读本文档。

Architecture map

架构图

  • /api/billing/checkout
    creates Stripe Checkout Sessions for Pro when
    STRIPE_SECRET_KEY
    is set. It sets
    client_reference_id
    to the Stack user id, auto-creates an anonymous Stack user for signed-out buyers, and falls back to the legacy Stack purchase path when Stripe is unset or
    plan=team
    . The "already active" short-circuit lives here.
  • /api/billing/portal
    resolves the current Stack user, looks up their
    stripe_customers
    row, and creates a Stripe customer portal session returning to
    /pricing
    .
  • /api/billing/subscription
    cancels or resumes the active Stripe Pro subscription;
    /dashboard/billing
    renders localized in-dashboard plan state and self-serve actions.
  • web/services/billing/purchase.ts
    is the shared idempotent recorder used by
    /api/billing/complete
    and
    /api/stripe/webhook
    . It attaches email to the purchaser, records
    billing_email_claims
    on conflict, and never cross-grants based on an unverified email.
  • cmuxPlan
    in Stack
    clientReadOnlyMetadata
    is the only entitlement VM code reads; a
    cmuxVmPlan
    manual override wins.
    resolveProPlanStatus
    ORs legacy Stack products with active
    stripe_subscriptions
    rows.
  • /api/stripe/webhook
    is signature-verified, insert-first idempotent through
    stripe_webhook_events
    , safe for foreign events in the shared Stripe account, and gates cmux handling on
    metadata.app === "cmux"
    . Return 2xx only after durable writes; return 500 to make Stripe retry.
  • /api/billing/checkout
    在设置
    STRIPE_SECRET_KEY
    后为专业版创建Stripe Checkout会话。它将
    client_reference_id
    设置为Stack用户ID,为未登录买家自动创建匿名Stack用户,当未设置Stripe或
    plan=team
    时回退到旧版Stack购买流程。“已激活”短路逻辑在此处实现。
  • /api/billing/portal
    解析当前Stack用户,查找其
    stripe_customers
    行,并创建返回至
    /pricing
    的Stripe客户门户会话。
  • /api/billing/subscription
    取消或恢复活跃的Stripe专业版订阅;
    /dashboard/billing
    渲染本地化的仪表盘内套餐状态及自助操作界面。
  • web/services/billing/purchase.ts
    /api/billing/complete
    /api/stripe/webhook
    共用的幂等记录器。它将邮箱附加到购买者,在冲突时记录
    billing_email_claims
    ,且绝不会基于未验证的邮箱跨账户授予权限。
  • Stack
    clientReadOnlyMetadata
    中的
    cmuxPlan
    是权限VM代码读取的唯一依据;手动覆盖的
    cmuxVmPlan
    优先级更高。
    resolveProPlanStatus
    会将旧版Stack产品与活跃的
    stripe_subscriptions
    行进行逻辑或运算。
  • /api/stripe/webhook
    经过签名验证,通过
    stripe_webhook_events
    实现插入优先的幂等性,可安全处理共享Stripe账户中的外部事件,并通过
    metadata.app === "cmux"
    控制cmux相关处理。仅在完成持久化写入后返回2xx状态码;返回500状态码可让Stripe重试。

Dev workflow

开发工作流

  • Use
    web/scripts/stripe/dev-stack.sh
    .
  • The tagged app bakes
    CMUX_PORT
    into
    Info.plist
    ; run the dev server on the tag's printed port, never a hardcoded one.
  • Per-branch Docker Postgres ports collide with other agents' containers. Use
    --db-port
    and never stop containers you did not create.
  • /app-pricing
    requires
    cmux_app=1
    .
    cmux_scheme
    threads the native deeplink return scheme;
    cmux-dev-*
    schemes are honored only for localhost requests.
  • Repeat dogfood: use a private window for a fresh anonymous buyer, and
    web/scripts/stripe/dev-reset.sh <email>
    to un-Pro a signed-in dev account before retesting checkout.
  • 使用
    web/scripts/stripe/dev-stack.sh
    脚本。
  • 打标签的应用会将
    CMUX_PORT
    写入
    Info.plist
    ;请在标签打印的端口上运行开发服务器,切勿使用硬编码端口。
  • 各分支的Docker Postgres端口会与其他代理的容器冲突。请使用
    --db-port
    参数,且切勿停止非你创建的容器。
  • /app-pricing
    需要设置
    cmux_app=1
    cmux_scheme
    用于传递原生深度链接返回协议;
    cmux-dev-*
    协议仅对localhost请求生效。
  • 重复内部测试:使用隐私窗口模拟新的匿名买家,并在重新测试结账前使用
    web/scripts/stripe/dev-reset.sh <email>
    脚本取消已登录开发账户的专业版权限。

Test-mode resources

测试模式资源

Product
prod_UpIQRE6cj0nFjs
. New checkouts use
cmux-pro-monthly
($30/mo) and
cmux-pro-yearly-288
($288/yr, equivalent to $24/mo). Keep
cmux-pro-yearly
($240/yr) active for grandfathered subscriptions. Staging webhook endpoint
we_1Tq1SZGhInAdn3JbWJReKNEN
forwards to
cmux-staging.vercel.app
; its secrets are already in the
cmux-staging
Vercel project.
产品ID为
prod_UpIQRE6cj0nFjs
。新结账使用
cmux-pro-monthly
(每月30美元)和
cmux-pro-yearly-288
(每年288美元,相当于每月24美元)。需保持
cmux-pro-yearly
(每年240美元)活跃,以支持老用户订阅。 staging环境webhook端点
we_1Tq1SZGhInAdn3JbWJReKNEN
会转发至
cmux-staging.vercel.app
;其密钥已配置在
cmux-staging
Vercel项目中。

Feature flags

功能开关

pro-upgrade-ui-enabled-release
(PostHog id
741838
) gates all Pro UI and stays OFF in release until launch; DEBUG builds default it on. Public Pro and Team pricing CTAs always route through
/api/billing/checkout
, never the download confirmation page.
cmux __internal_flags
, once merged, inspects and overrides flags locally.
pro-upgrade-ui-enabled-release
(PostHog ID:
741838
)控制所有专业版UI,发布前保持关闭;DEBUG版本默认开启。公开的专业版和团队版定价CTA始终通过
/api/billing/checkout
路由,绝不会跳转至下载确认页面。
cmux __internal_flags
命令在合并后可用于本地查看和覆盖开关状态。

Prod runbook

生产环境运行手册

Run
web/scripts/stripe/provision-live.sh
with an operator key, add the two Vercel envs, deploy, validate live with a 100-percent-off promotion code purchase, then cancel.
DB migrations:
bun run cloud-vm:preflight
,
bun run cloud-vm:migrate -- staging
, staging deploy, then
bun run cloud-vm:migrate -- production
. Never run migrations from builds. See the Cloud VM ops flow.
使用操作员密钥运行
web/scripts/stripe/provision-live.sh
脚本,添加两个Vercel环境变量,部署后使用100%折扣码进行购买验证,然后取消订阅。
数据库迁移:执行
bun run cloud-vm:preflight
bun run cloud-vm:migrate -- staging
,部署staging环境,再执行
bun run cloud-vm:migrate -- production
。切勿从构建版本中运行迁移。请查看Cloud VM操作流程。

Gotchas

注意事项

  • bun mock.module
    is process-global, so every module mock must carry every real export other suite files import. A missing export can surface only in CI's test order as
    Export named X not found
    .
  • Tests must not depend on
    DATABASE_URL
    being set.
  • drizzle-1.0-beta wraps pg errors in
    DrizzleQueryError
    ; read
    error.cause
    for the pg
    code
    and
    constraint
    .
  • Pages outside
    app/[locale]
    need a
    proxy.ts
    bypass (like
    /app-pricing
    and
    /billing
    ), or
    next-intl
    rewrites them into the locale tree and they 404 through missing root layout tags. Those subtrees also need their own layout with
    html
    and
    body
    .
  • bun mock.module
    是进程全局的,因此每个模块模拟必须包含其他测试套件文件导入的所有真实导出。缺失的导出仅会在CI测试顺序中以
    Export named X not found
    错误形式显现。
  • 测试不得依赖
    DATABASE_URL
    已配置。
  • drizzle-1.0-beta会将pg错误包装在
    DrizzleQueryError
    中;需读取
    error.cause
    获取pg的
    code
    constraint
    信息。
  • app/[locale]
    之外的页面需要
    proxy.ts
    绕过(如
    /app-pricing
    /billing
    ),否则
    next-intl
    会将其重写至区域树中,因缺少根布局标签而返回404。这些子树还需要各自包含
    html
    body
    的布局。