Unified Platform Host

Next.js + Swift Unified Setup

约 30 分钟把 Next.js + Swift 跑成同一个产品——共享用户、待办和订阅。

本示例把 Next.js + Swift 从两份全新 checkout 带到可用的 Unified Mode:同一套 Better Auth 用户、跨端的 todos 与 Profile,以及任一端购买都会 解锁的同一行订阅。

按顺序执行——每一步都是下一步的依赖。核心闭环(第 1–5 步)约 30 分 钟;支付与推送(第 6–7 步)耗时更多只是因为涉及第三方后台。

开始之前

  • 两个模板均已 checkout(soar-nextsoar-swift)且各自可以构建——见 Next 安装Swift 文档
  • 一个可连接的 PostgreSQL 数据库(本地或托管)。使用专用数据库——不要 与其他项目共用。
  • Xcode 16+、Node 20+、pnpm。

先建数据库

soar-next.env 中设置 DATABASE_URL,然后推送 Schema:

pnpm exec drizzle-kit push

一次推送即创建 Unified Mode 需要的全部表——Better Auth 表、todosdevice_tokens 表,以及 subscription 表。没有单独的 Better Auth 迁移步 骤。生产环境请改用生成式迁移而非 push——见 数据库

核心服务端环境变量

仍在 soar-next/.env

变量
BETTER_AUTH_SECRET足够长的随机串(openssl rand -base64 32
BETTER_AUTH_URL服务自身 URL——本地为 http://localhost:3000
APP_SCHEMEsoar://——iOS App 每个请求都以它作为 Origin
RESEND_API_KEY原生注册/重置依赖的 Email OTP 验证码邮件需要它

APP_SCHEME 是两个 App 之间的握手:Swift 客户端硬编码发送 Origin: soar://,Better Auth 会用 403 拒绝未注册的 Origin。启动 pnpm dev,确认 Web 端可以注册和登录。

OAuth 客户端(一个 Google 项目、一个 Apple ID)

这里顺序很重要——所有客户端都创建在同一个 Google Cloud 项目里,共用一 个 consent screen:

  1. Google Web 客户端GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET (服务端 .env)——支撑 Web 重定向登录。
  2. Google iOS 客户端(填 App 的 Bundle ID)→ GOOGLE_IOS_CLIENT_ID 同时写进服务端(audience 白名单) Swift 的 Secrets.xcconfig(连同回调 scheme GOOGLE_REVERSED_CLIENT_ID)。
  3. Apple:把 APPLE_APP_BUNDLE_IDENTIFIER(服务端 .env)设为 App 的 Bundle ID,并在 Xcode 中启用 Sign in with Apple capability。不需要 Services ID,也不需要 Apple OAuth secret。

细节与校验规则见原生认证

把 Swift App 指向 API

soar-swift

cp Secrets.example.xcconfig Secrets.xcconfig
SOAR_BACKEND_MODE = soar_api
SOAR_API_BASE_URL = http:/$()/localhost:3000

$()// 拆开,避免 xcconfig 当成注释;真机运行时用你电脑的地址替代 localhost。)SUPABASE_* 键保持原样即可——Unified Mode 永远不会构建 Supabase 客户端。这个开关的含义见后端模式

第一次跨端冒烟测试

  1. 在 iOS 模拟器注册(邮箱 + OTP 验证码,或 Google)。
  2. 用同一账户登录 Web 端。
  3. 在 Web 创建一条 todo → iOS 下拉刷新 → 它出现了。
  4. 在 iOS 修改它 → 刷新 Web → 它变了。
  5. 任一端更新 Profile,确认另一端能读到。

登录报 403 就检查 APP_SCHEME;API 调用报配置错误就检查 SOAR_API_BASE_URL。至此身份与数据闭环已经打通——下面只是加上钱和推送。

支付:Creem/Stripe(Web)+ RevenueCat(iOS)

先配 Web 侧,再配 RevenueCat,最后验证两者汇入同一行 subscription

  1. Web Checkout:按支付配置 Creem(或 Stripe) API key、产品 ID 和 Webhook secret。本地用 ngrok/cloudflared 转发 Webhook,购买结果才能落库。
  2. RevenueCat 产品:创建 App Store 产品并设置三个显式映射—— REVENUECAT_PRODUCT_PRO_MONTHLY_PRO_YEARLY_LIFETIME。产品名从 不参与猜测。
  3. RevenueCat Webhook:指向 https://<host>/api/payment/notify/revenuecat,Authorization header 与 REVENUECAT_WEBHOOK_AUTHORIZATION 完全一致
  4. Restore Behavior:在 RevenueCat 项目设置中保持禁用 Transfer。 服务端会刻意忽略 TRANSFER 事件并记为配置漂移警告;收据冲突走支持流 程,而不是悄悄转移权益。
  5. REVENUECAT_IOS_KEY 写进 Swift 的 Secrets.xcconfig;App 用 Better Auth 用户 ID 登录 RevenueCat——Webhook 正是靠它把购买映射回共享用户。

用 Sandbox 购买验证双向:Web 结账 → iOS 显示已解锁(并隐藏 App Store 购买 按钮);iOS Sandbox 内购 → Web 账户页出现订阅。两条链路写的是同一行—— 见订阅

存储与推送(面向生产就绪)

  • 上传:本地开发用 STORAGE_PROVIDER=local 即可。staging 与生产设置 STORAGE_PROVIDER=r2R2_* 键——见上传
  • 推送:设置 APNS_TEAM_IDAPNS_KEY_IDAPNS_BUNDLE_IDAPNS_PRIVATE_KEY,然后用 App 的调试推送页面给自己发一条测试通知—— 见推送通知

环境变量检查表

Unified Mode 读取的全部配置,一处看全:

位置变量何时必需
soar-next/.envDATABASE_URLBETTER_AUTH_SECRETBETTER_AUTH_URLAPP_SCHEMERESEND_API_KEY始终
soar-next/.envGOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRETGOOGLE_IOS_CLIENT_IDAPPLE_APP_BUNDLE_IDENTIFIEROAuth 登录
soar-next/.envCreem/Stripe 键 + REVENUECAT_WEBHOOK_AUTHORIZATION + REVENUECAT_PRODUCT_*支付
soar-next/.envSTORAGE_PROVIDER(+ R2_*)、APNS_*上传 / 推送
soar-swift/Secrets.xcconfigSOAR_BACKEND_MODE=soar_apiSOAR_API_BASE_URL始终
soar-swift/Secrets.xcconfigGOOGLE_IOS_CLIENT_IDGOOGLE_REVERSED_CLIENT_IDREVENUECAT_IOS_KEYGoogle 登录 / 内购

Staging 与生产检查表

  • soar-next 部署到 staging 主机;设置 BETTER_AUTH_URL,并把 Creem/Stripe 与 RevenueCat 的 Webhook 重新指向它。
  • 构建一个 Swift staging 配置,SOAR_API_BASE_URL 指向 staging 主机 (HTTPS)。
  • 切到 STORAGE_PROVIDER=r2,端到端验证一次上传。
  • App Store 构建使用生产 APNs(apns.push.apple.com);sandbox token 与生 产 token 不可互换。
  • Schema 变更通过生成式迁移应用,不用 drizzle-kit push
  • 确认 RevenueCat Restore Behavior 仍为禁用 Transfer,且认证限流已生效 (生产环境自动启用)。

常见错误

症状原因 / 处理
登录报 403 MISSING_OR_NULL_ORIGINAPP_SCHEME 缺失或不是 soar://——必须与客户端硬编码的 Origin 一致。
每个 API 调用都报配置错误SOAR_BACKEND_MODE=soar_apiSOAR_API_BASE_URL 未设置——这是刻意行为,不会静默回退 Supabase。
原本正常,突然全部 401 UNAUTHENTICATEDSession 被撤销或过期;App 会清理 Keychain token 并要求重新登录。不要用 /api/auth/get-session 的状态码判断认证——它返回 200 + null
Google 登录被服务端拒绝Token 的 audience 不在白名单——服务端GOOGLE_IOS_CLIENT_ID,或客户端建在了不同的 Google 项目里。
iOS 内购迟迟不同步到 WebRevenueCat Webhook URL/Authorization 不匹配,或所购产品缺少对应的 REVENUECAT_PRODUCT_* 映射。
Web 结账被「已有订阅」拦下符合设计:任一渠道的有效订阅都会阻止第二次购买,直到到期。
测试推送提示 token 无效Sandbox/生产不匹配——Debug 构建的 token 需要 sandbox APNs 环境。

相关页面

本页目录