前言
這次要搬的是一個有後台頁面、OAuth Session、Webhook、App Proxy、Discount Function 與批量改價功能的 Shopify App。原本的 Web App 由長駐 Node process 提供服務,資料存在 PostgreSQL;目標則是把 React Router SSR 放到 Vercel Functions,資料庫移到 Neon。
表面上看起來只要重新部署程式,再換掉 database connection 就完成了。真正開始盤點後,卻發現至少有四個不同問題:Vercel 不執行原本的常駐 server、serverless instance 會放大資料庫連線數、migration 不能在每次 cold start 執行,而 Shopify 的 App URL 與 callback 也必須在正確時間切換。
上一篇談的是 Shopify App Billing 的 route access control。這篇接著處理 App 本身搬家時,程式、資料與正式流量如何分段切換。
原本的做法與問題症狀
舊部署模型很直覺:
build React Router
↓
執行 Prisma migration
↓
啟動 react-router-serve
↓
同一個 Node process 長時間接 request
這套流程適合有 release/start phase 的長駐主機,但不能原封不動搬到 Vercel。Vercel 會把 SSR routes 轉成 Functions,不會替專案保留一個永久運作的 react-router-serve process。
資料庫也不是只把連線字串指向 Neon。Function 可以水平擴充,每個 instance 都可能建立自己的 Prisma connection;若 runtime 全部使用 direct connection,流量高峰容易把短連線直接推向 database。
另一個症狀是 migration 與 runtime 的責任混在一起。長駐 container 可以在啟動前跑一次 prisma migrate deploy,但 serverless cold start 不是 release phase。讓每個 instance 都嘗試 migration,會帶來競爭、延遲與權限過大的 runtime secret。
問題是怎麼推敲出來的?
先從 request path 盤點,這個 App 沒有自建常駐 worker、WebSocket server 或記憶體內排程器;核心請求都是 React Router loader/action、Webhook 與 App Proxy。這表示它適合改成事件驅動的 Functions,但 entry server 必須換成 Vercel 支援的 React Router handler。
接著查 Git 演進。遷移 commit 加入 @vercel/react-router preset,並把自訂 entry.server 改用 Vercel handler;Shopify 的 response headers 仍在呼叫 handler 前加入。這不是把原 entry 刪掉,而是保留 Shopify embedded app 所需的安全標頭,再換掉底層 rendering adapter。
資料層則分成兩種連線:
| 工作 | 連線角色 |
|---|---|
| Function runtime 的 Prisma query | pooled connection |
| schema migration、dump/restore | direct connection |
Neon 官方目前仍建議 serverless runtime 使用 pooled endpoint,migration 與管理操作使用 direct endpoint。來源專案第一階段沿用一般 Prisma Client,尚未導入 @prisma/adapter-neon;因此 adapter 只能列為後續優化,不能倒過來寫成已完成。
最後再看正式切換。Git history 能證明 staging 與 production 的 Shopify App URL/callback 設定曾依序更新,但 repository 不能單獨證明資料 row count、cold-start p99 或所有商店驗收都已完成。文章因此只把可驗證的程式與切換設計寫成完成式,把平台實測保留為限制。
最後採用的架構
Web build 分成一般環境與 Vercel 環境:
const isVercelBuild = process.env.VERCEL_BUILD === '1';
export default {
ssr: true,
presets: isVercelBuild ? [vercelPreset()] : [],
};
這讓原本的 build path 可以保留,同時讓 Vercel build 產生平台理解的 route bundles。自訂 entry server 則交給 @vercel/react-router/entry.server,但仍先執行 Shopify response header helper。
Database migration 被移出 application startup,改成受環境保護、手動觸發且有 concurrency guard 的 CI job。Runtime 只拿執行 query 所需的 pooled connection;migration job 才取得 direct connection。
正式流量採兩段切換:
舊 Web + 舊 PostgreSQL
↓
舊 Web + Neon
↓
Vercel Web + Neon
先固定 Neon 為新的資料權威,再把 Shopify App URL 切到 Vercel。若新 Web host 發生問題,可以只回切 Web;新舊 Web 都指向同一個 Neon,不會因為回滾 host 而遺失切換後的資料。
關鍵實作
Vercel build 與一般 build 分開,避免把 hosting provider 的 adapter 強加到所有環境:
{
"scripts": {
"build": "prisma generate && react-router build",
"build:vercel": "prisma generate && VERCEL_BUILD=1 react-router build"
}
}
Migration workflow 的重點不是 YAML 語法,而是權限與次數:
人工選擇 staging / production
↓
對應 environment approval
↓
同一環境禁止並行 migration
↓
direct database connection
↓
prisma migrate deploy
App host 切換時,hosting environment 的 App URL、Shopify app configuration 的 application_url 與 OAuth callback 必須一起對齊。任何 secret、connection string 或平台識別碼都只留在平台 secret store,不進 Git。
實際驗證
來源專案在適配 commit 中記錄了以下本機驗證:
- TypeScript typecheck。
- ESLint。
- 25 個測試檔、202 個測試。
- 一般 React Router build。
- Vercel preset build。
- 原本 container build 與 health endpoint。
- Prisma schema 與 migration 狀態檢查。
Git history 也能看到 staging 設定先完成,production App URL/callback 再於另一個 commit 切換,符合先驗證再切正式設定的順序。
但沒有公開、可重播的 production database row count、Webhook cold-start p99 或最大批量特價壓測結果,因此這些不列為「已驗證完成」。
仍然存在的限制
Pooled connection 只能管理連線壓力,不會自動解決慢 query、過長 transaction 或跨 request 狀態。Neon 使用 transaction pooling 時,也不能假設 session-level setting 會在下一個 transaction 保留。
來源專案目前仍使用一般 Prisma Client;是否改成 Neon driver adapter,要另外驗證 bundle、transaction 與既有測試,不能在第一次搬遷同時擴大改動面。
Database 開始接受新寫入後,回滾不能只把 connection 指回舊資料庫。必須先比對新寫入或從新 authority 建立恢復方案。
最後,Web deployment Ready 不代表 Shopify Function、Theme Extension 或 app configuration 已發布。這正是下一篇要拆開的問題。
可以延伸到哪些情境?
這次遷移可以整理成三個原則:
- 先列出執行模型差異,再選 hosting adapter。
- 把 runtime query 與 schema migration 的連線、權限和時機分開。
- 先固定資料權威,再切 Web 流量,讓回滾不需要倒退資料。
這些原則同樣適用於把 containerized SaaS 搬到 Functions、把單機 PostgreSQL 換成 serverless database,或把多個部署步驟拆成可獨立驗證的 release pipeline。
下一篇會把同一次發布拆成四個不同平面:Shopify App 其實有四個部署面:Web、App Version、Database 與 Store Config
參考資料
- Vercel:React Router on Vercel
- Neon:Connection pooling
- Prisma:Neon guide
- Shopify:Deploy to a hosting service
以上平台文件查核日期:2026-07-25。
