> ## Documentation Index
> Fetch the complete documentation index at: https://supastack.dev/llms.txt
> Use this file to discover all available pages before exploring further.
> For the full documentation corpus in one file, fetch: https://supastack.dev/llms-full.txt

# 把现有 SaaS 迁入 supastack

一份分阶段的操作手册：把正在运转的 SaaS —— 认证、租户、计费、任务 —— 迁移到 supastack 地基上，不重写，也不需要停机一个周末。

_发布于 2026-10-29._
_更新于 2026-10-29._

> **如何把现有的 SaaS 迁移到 supastack？**
> 这取决于你的代码库，但迁移顺序不变：分阶段迁移，绝不一气重写。把仓库盘点为产品代码（路由、schema、领域逻辑——仍是你的）和基础设施（认证、租户、计费、任务、邮件——由套件替换），按依赖顺序接入模块：结构、认证、工作区、计费，然后是后台任务。用可重跑的脚本迁移数据，保留 Stripe 客户 ID，验收通过后再切换。大多数 Web 应用只需要几个晚上，而不是几周。

## 动代码之前先盘点

每一次搞砸的迁移都以同样的方式开始：有人打开套件仓库就开始删文件。套件是地基，不是主题——它替换的是你的基础设施，而对你的产品没有任何意见。在动任何东西之前，诚实地把仓库分成两堆。

产品代码是客户能向你复述的一切：你的路由、模式、领域逻辑、文案、定价页。基础设施是他们永远看不到的一切：会话、租户检查、支付 Webhook、排队任务、事务邮件。第一堆原封不动地迁走。第二堆被替换——这正是全部意义所在。

- 列出每个路由组，标注是产品还是管道——别手软；只用来切换 Webhook URL 的设置页也是管道。
- 列出你的环境变量，标出哪些配置基础设施（认证密钥、供应商密钥）、哪些改变产品行为。
- 列出你的表，标出哪些将由套件拥有（用户、会话、工作区、计费状态）、哪些是你的。
- 写下那条为你带来收入的用户旅程——这条流程就是下面每个阶段的验收测试。


## 把每条接缝映射到套件模块

套件的基础设施是一组以契约相连的模块——你的工作是把手头每一段旧管道指向现在拥有它的模块。大多数映射是一一对应的；对不上的地方，才是你真正要做决策的地方。

有两处映射选择值得真正思考，而不是机械照搬。如果你来自 `Prisma`，套件标准化在 Drizzle 上——过渡期内产品表可以继续用 Prisma，但要计划收敛；一个代码库里两个 ORM 是会利滚利的维护税。[Prisma vs Drizzle 对比](https://supastack.dev/zh/blog/prisma-vs-drizzle-for-saas.md)讲清了会发生什么变化。

如果你来自托管认证厂商，忍住那种想在切换前把厂商功能全部复刻一遍的冲动。托管认证悄悄捆绑了你的产品可能不用的东西——会话列表、组织 UI、Webhook 同步。只映射你的产品实际调用的东西；[认证对比](https://supastack.dev/zh/blog/better-auth-vs-nextauth-vs-clerk.md)覆盖了那些真正要紧的能力差异。

| 你现在的管道 | supastack 模块 | 迁移过来的东西 |
|---|---|---|
| NextAuth.js / Auth.js 会话 | 认证（Better Auth） | 用户与账户迁移；会话在切换时重新签发 |
| Clerk 或托管认证厂商 | 认证（Better Auth） | 用户按邮箱迁移；第三方会话被放弃，而不是重放 |
| 自建组织/团队表 | 工作区 | 你的组织行变成工作区 + 成员关系行 |
| 手搓的 Stripe Webhook | 计费 | Stripe 客户 ID 保留；订阅状态重新推导 |
| CRON + setTimeout 任务 | 任务（Trigger.dev） | 定时工作移到共享的 JobRunner 契约之后 |
| Nodemailer / 临时邮件 | 邮件 | 模板迁走；发送走同一个供应商适配器 |


## 按依赖顺序采用模块

迁移顺序不是偏好问题——它是依赖图。下面的每个阶段都可以单独发布，并且每个阶段都产出一个在下一阶段开始前可验证的可工作系统。诱惑是在一个周末做完整个事情、然后调试它们的交集；忍住——分阶段迁移意味着你遇到的每个 bug 都恰好只属于一个模块，这是一种调试超能力，跳过它之前你不会意识到它的价值。

- 阶段 1——骨架：搭起套件脚手架，配置环境变量，把产品路由放进项目结构。还没有任何接线；应用能启动，页面能渲染。
- 阶段 2——认证：把登录迁到 Better Auth 上，迁移用户/账户表，端到端验证登录 + 退出。之后的每个阶段都依赖会话的存在。
- 阶段 3——租户：采用工作区模型，在产品表上回填 `workspaceId`，在读写中强制执行范围限定。[工作区边界](https://supastack.dev/zh/blog/workspace-boundaries.md)一文正是在这里发挥作用。
- 阶段 4——计费：把结账和 Webhook 指向计费模块，重新映射现有订阅，用测试模式下的真实 Stripe 数据验证权益检查。
- 阶段 5——后台任务与邮件：把任务移植到 JobRunner 契约上，把事务邮件迁到邮件适配器。它们放在最后，因为它们依赖用户和工作区已经存在。


## 诚实地迁移数据

数据迁移正是迁移名声的来源。让它保持无聊的规则：写一个可以跑两遍的脚本；切换前绝不动源数据；验证条数而不是相信脚本。

用户和账户落在 Better Auth 模式里——邮箱是连接键，基于密码的账户需要做个决定：要么你现有的哈希格式是 Better Auth 能验证的，要么在切换时上线一个重置密码流程，把它当作强制手段。第三方 OAuth 账户是简单的情形——供应商会在下次登录时重新签发。

对计费来说，诚实的路径是重新推导而不是复制行：保留 Stripe 客户 ID，让套件的 Webhook 处理用供应商的真实状态重建权益，再把它算出的结果与你现有的订阅记录做对比。套件的[幂等 Webhook 账本](https://supastack.dev/zh/blog/idempotent-billing-webhooks.md)正是为此而存在的——被重放的历史事件不会重复供应任何东西。

```
// migration scripts are boring by design: upsert-shaped,
// re-runnable, and they verify counts instead of trusting themselves
for (const legacyUser of legacyUsers) {
  await upsertAuthUser({
    email: legacyUser.email.toLowerCase(),
    name: legacyUser.displayName,
    externalId: legacyUser.id, // preserved for one-shot joins
  });
}
const migrated = await countAuthUsers();
if (migrated !== legacyUsers.length) {
  throw new Error(`count mismatch: ${migrated} of ${legacyUsers.length}`);
}
```


## 属于你的部分不变

这点值得直说，因为人们两个方向都会搞错：你的产品路由、模式、领域逻辑和设计都不变。你不是在套件里面重建应用——而是把你的应用安放在它上面。

反方向的错误是让旧管道在新模块旁边“暂时”活着。第二个会话检查、第二个 Webhook 处理器、第二张权益表——每个“临时”的并行系统在有人删除它之前都是永久的，而每一个都是未来的事故。替换、验证、删除。如果某个阶段还不能删掉旧路径，它就还没完成。


## 切换检查清单

切换是一份检查清单，不是一个事件。在真正的日期之前，先在预发布环境里用接近生产的数据至少完整跑一遍。

- 用一份新的生产快照重跑数据脚本；条数与抽查全部通过。
- 用产品支持的每个认证供应商登录一遍——包括那些你忘了它们存在的。
- 在 Stripe 测试模式下购买自己的产品，跟着 Webhook 一路追到权益生效。
- 每封事务邮件都发一次；检查链接落在新的部署上。
- 端到端验证付费旅程，然后切 DNS/环境变量，看着第一个真实会话进来。
- 让旧部署再跑一周——回滚是一次 DNS 修改，不是一次迁移。


## 关于迁入脚手架的常见问题

每当有人坐下来做这件事，出现的总是同一批问题。

- 迁移实际要花多久？ —— 标准认证 + Stripe 的单个 Web 应用：是几个晚上，不是几周。带自定义计费的多租户 B2B 会在第三、四阶段加时间——拉长的是数据工作，从来不是文件搬运。
- 我该迁移，还是从头开始再移植功能？ —— 如果你的产品代码比基础设施债更小，在套件上从头开始、移植产品路由通常更快。如果模式和领域逻辑才是难点，就原地迁移。
- 过渡期能保留现有 ORM / 认证库吗？ —— 短暂保留，可以——一次一个模块边界。永久保留，不行：基础设施层里任何东西有两份都是利滚利的税。
- 已经在路上的 Stripe Webhook 怎么办？ —— 第四阶段验证通过后，把现有 Webhook 端点指向套件的处理器；幂等账本让重放或重复的事件变得安全而不是危险。
- 必须采用每个模块吗？ —— 不必。移动端、桌面端、任务和 AI 是可以采用也可以无视的端面——许可证给你的是地基，不是每个平台都必须发一个应用的命令。
