如何把現有的 SaaS 遷移到 supastack?
這取決於你的程式碼庫,但遷移順序不變:分階段遷移,絕不一氣重寫。把倉庫盤點為產品程式碼(路由、模式、領域邏輯——仍是你的)和基礎設施(認證、租戶、計費、任務、郵件——由套件替換),按依賴順序接入模組:結構、認證、工作區、計費,然後是後臺任務。用可重跑的腳本遷移資料,保留 Stripe 客戶 ID,驗收通過後再切換。大多數 Web 應用只需要幾個晚上,而不是幾週。
動程式碼之前先盤點
每一次搞砸的遷移都以同樣的方式開始:有人打開套件倉庫就開始刪檔案。套件是地基,不是主題——它替換的是你的基礎設施,而對你的產品沒有任何意見。在動任何東西之前,誠實地把倉庫分成兩堆。
產品程式碼是客戶能向你複述的一切:你的路由、模式、領域邏輯、文案、定價頁。基礎設施是他們永遠看不到的一切:會話、租戶檢查、支付 Webhook、排隊任務、事務郵件。第一堆原封不動地遷走。第二堆被替換——這正是全部意義所在。
- 列出每個路由組,標註是產品還是管道——別手軟;只用來切換 Webhook URL 的設定頁也是管道。
- 列出你的環境變數,標出哪些配置基礎設施(認證金鑰、供應商金鑰)、哪些改變產品行為。
- 列出你的表,標出哪些將由套件擁有(使用者、會話、工作區、計費狀態)、哪些是你的。
- 寫下那條為你帶來收入的使用者旅程——這條流程就是下面每個階段的驗收測試。
把每條接縫映射到套件模組
套件的基礎設施是一組以契約相連的模組——你的工作是把手邊每一段舊管道指向現在擁有它的模組。大多數映射是一一對應的;對不上的地方,才是你真正要做決策的地方。
有兩處映射選擇值得真正思考,而不是機械照搬。如果你來自 Prisma,套件標準化在 Drizzle 上——過渡期內產品表可以繼續用 Prisma,但要計畫收斂;一個程式碼庫裡兩個 ORM 是會利滾利的維護稅。Prisma vs Drizzle 對比講清了會發生什麼變化。
如果你來自託管認證廠商,忍住那種想在切換前把廠商功能全部複刻一遍的衝動。託管認證悄悄捆綁了你的產品可能不用的東西——會話列表、組織 UI、Webhook 同步。只映射你的產品實際呼叫的東西;認證對比覆蓋了那些真正要緊的能力差異。
| 你現在的管道 | 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,在讀寫中強制執行範圍限定。工作區邊界一文正是在這裡發揮作用。 - 階段 4——計費:把結帳和 Webhook 指向計費模組,重新映射現有訂閱,用測試模式下的真實 Stripe 資料驗證權益檢查。
- 階段 5——後臺任務與郵件:把任務移植到 JobRunner 契約上,把事務郵件遷到郵件介面卡。它們放在最後,因為它們依賴使用者和工作區已經存在。
誠實地遷移資料
資料遷移正是遷移名聲的來源。讓它保持無聊的規則:寫一個可以跑兩遍的腳本;切換前絕不動源資料;驗證筆數而不是相信腳本。
使用者和賬戶落在 Better Auth 模式裡——郵箱是連接鍵,基於密碼的賬戶需要做個決定:要麼你現有的雜湊格式是 Better Auth 能驗證的,要麼在切換時上線一個重設密碼流程,把它當作強制手段。第三方 OAuth 賬戶是簡單的情形——供應商會在下次登入時重新簽發。
對計費來說,誠實的路徑是重新推導而不是複製行:保留 Stripe 客戶 ID,讓套件的 Webhook 處理用供應商的真實狀態重建權益,再把它算出的結果與你現有的訂閱記錄做對比。套件的冪等 Webhook 賬本正是為此而存在的——被重放的歷史事件不會重複供應任何東西。
// 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 是可以採用也可以無視的端面——許可證給你的是地基,不是每個平臺都必須發一個應用的命令。