前言

這次要搬的是一個有後台頁面、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 已發布。這正是下一篇要拆開的問題。

可以延伸到哪些情境?

這次遷移可以整理成三個原則:

  1. 先列出執行模型差異,再選 hosting adapter。
  2. 把 runtime query 與 schema migration 的連線、權限和時機分開。
  3. 先固定資料權威,再切 Web 流量,讓回滾不需要倒退資料。

這些原則同樣適用於把 containerized SaaS 搬到 Functions、把單機 PostgreSQL 換成 serverless database,或把多個部署步驟拆成可獨立驗證的 release pipeline。

下一篇會把同一次發布拆成四個不同平面:Shopify App 其實有四個部署面:Web、App Version、Database 與 Store Config

參考資料

以上平台文件查核日期:2026-07-25。