| audience | both |
|---|---|
| applies-to | post-scaffold |
從開發到 Production 的完整部署流程(Cloudflare Workers)。
- Cloudflare 帳號(免費方案即可)
- GitHub 帳號(用於 CI/CD)
- Supabase Cloud 專案(或免費 Self-host,Supabase 是開源的)
Supabase 是開源的,除了使用官方 Cloud 服務,也可以免費自架在自己的伺服器上。兩條路線的開發方式完全相同(Migration、RLS、API 都一樣),差別在部署與維運:
| 比較項目 | Cloud | Self-hosted |
|---|---|---|
| 適合情境 | 快速上線、不想管基礎設施 | 資料隱私要求高、長期成本控制 |
| 費用 | 免費方案有限額,Pro $25/月起 | 免費(僅付伺服器費用) |
| 維運 | Supabase 負責升級、備份、監控 | 自行維護 Docker Compose、備份、升級 |
| Migration | supabase db push 一鍵推送 |
docker exec 或 psql 手動執行 |
| Dashboard | supabase.com/dashboard |
自架 Studio(localhost:3000) |
建議:剛開始用 Cloud 免費方案快速開發,之後可隨時遷移到 Self-hosted。程式碼不需要任何修改,只需更換環境變數。
- 選 Cloud → 繼續下方 Step 1
- 選 Self-hosted → 前往 Self-hosted Supabase 部署指南,完成後跳到 Step 2
Read this before deploying. If you are using self-hosted Supabase with Cloudflare Workers, your choice of auth solution has critical implications. This section explains the constraints and helps you pick the right path.
Better Auth stores sessions, users, and accounts in PostgreSQL. It requires a TCP connection to the database via the pg driver's connection pool.
- Supabase Cloud exposes a public PostgreSQL endpoint. Workers can TCP-connect directly. Better Auth works fine.
- Self-hosted Supabase (e.g., on a private VM behind Cloudflare Tunnel) does not expose a public PostgreSQL port. Workers'
connect()API fails withError: proxy request failed, cannot connect to the specified address. All auth operations return 500.
This is not a code bug -- it is a fundamental networking constraint. Cloudflare Workers can only establish TCP connections to publicly routable addresses, not to origins behind Cloudflare Tunnel.
| Deployment Target | Database | Better Auth | nuxt-auth-utils | Notes |
|---|---|---|---|---|
| Cloudflare Workers | Supabase Cloud | Works | Works | No restrictions |
| Cloudflare Workers | Self-hosted (public IP) | Works | Works | DB port must be publicly accessible |
| Cloudflare Workers | Self-hosted (Tunnel only) | Fails | Works | TCP blocked -- see workarounds below |
| Cloudflare Workers + Hyperdrive | Self-hosted (Tunnel only) | Works* | Works | *Requires Workers Paid plan ($5/mo); known timeout issues (#2274) |
| Node.js / VM | Any | Works | Works | Full TCP access, no restrictions |
If you want to keep Better Auth with a self-hosted database behind Cloudflare Tunnel:
Hyperdrive proxies TCP database connections from Workers. Configure it to point to your self-hosted PostgreSQL:
npx wrangler hyperdrive create my-db --connection-string="postgresql://user:pass@db.example.com:5432/postgres"Then use the Hyperdrive connection string in your Better Auth config.
Caveats:
- Requires Workers Paid plan ($5/month minimum)
- Free plan allows only 1 Hyperdrive config
- There are known connection timeout issues in production workloads
Open port 5432 (or a custom port) on your host with proper firewall rules and pg_hba.conf restrictions. This removes the Tunnel limitation but increases your attack surface.
If your project only needs OAuth login + session management (no 2FA, team management, rate limiting, etc.), nuxt-auth-utils is a simpler and more compatible alternative. See the migration guide below.
| nuxt-auth-utils | Better Auth | |
|---|---|---|
| Session storage | Encrypted cookie (no DB needed) | PostgreSQL table (requires TCP) |
| DB requirement | Optional (only to persist user profiles) | Required (session/user/account tables) |
| Workers compatibility | Full (HTTP + cookies only) | Requires TCP to DB (or Hyperdrive) |
| Features | Basic: OAuth, password login, 15+ providers | Rich: 2FA, teams, roles, rate limiting, 27+ plugins |
| Complexity | Low (composable + server routes) | Medium (DB adapter, migrations, schema) |
| When to choose | OAuth + session is enough; edge deployment; self-hosted DB | Need advanced auth features; have public DB endpoint |
If you decide to migrate, here is the high-level process:
pnpm add nuxt-auth-utilsAdd to nuxt.config.ts:
export default defineNuxtConfig({
modules: ['nuxt-auth-utils'],
})Add NUXT_SESSION_PASSWORD to your environment (minimum 32 characters):
openssl rand -base64 32- Remove Better Auth's
server/api/auth/[...all].tshandler - Create OAuth handlers using
nuxt-auth-utilspattern:
// server/routes/auth/google.get.ts
export default defineOAuthGoogleEventHandler({
async onSuccess(event, { user }) {
await setUserSession(event, {
user: {
/* ... */
},
})
return sendRedirect(event, '/')
},
})- import { useUserSession } from '~/composables/useAuth' // Better Auth
+ const { loggedIn, user, session, clear } = useUserSession() // nuxt-auth-utils- const session = await getUserSession(event) // Better Auth
+ const { user } = await requireUserSession(event) // nuxt-auth-utilspnpm remove better-auth @better-auth/cliRemove Better Auth database tables (user, session, account, verification) via a migration if no longer needed.
- Remove
BETTER_AUTH_SECRETfrom environment variables - Remove Better Auth config files (
auth.ts,auth.client.ts, etc.) - Update
NUXT_SESSION_PASSWORDin your GitHub Secrets
If you are running self-hosted Supabase behind Cloudflare Tunnel, watch out for these issues discovered in production:
-
Supabase API returns 522: If your reverse proxy (Caddy/Nginx) resolves
localhostto IPv6, and Docker's IPv6 port mapping is broken, Kong won't respond. Fix: use127.0.0.1explicitly, or route through Cloudflare Tunnel instead of direct connection. -
CSRF failures with Better Auth: Better Auth's
@better-fetch/fetchdoes not use Nuxt's$fetch, sonuxt-security's CSRF plugin cannot patch it. You must manually add CSRF headers increateAuthClient'sfetchOptions.onRequest. -
Tunnel DNS must be proxied: Both TCP and HTTP tunnel DNS records must have the orange cloud (proxied) enabled. If your Supabase API was previously using an A record pointing directly to the VM, you need to change it to a CNAME pointing to the tunnel.
-
Caddy TLS handshake failures: Caddy v2.10+ with Cloudflare Origin Certificates may need explicit
protocols tls1.2 tls1.3in the TLS config. Without it, TLS 1.3 handshake fails silently.
- 前往 Supabase Dashboard 建立新專案
- 取得連線資訊:
SUPABASE_URL:Project Settings → API → Project URLSUPABASE_KEY:Project Settings → API → anon public keySUPABASE_SECRET_KEY:Project Settings → API →service_rolekey
- 連結本地專案:
supabase link --project-ref <your-project-ref>- 推送 Migration 到遠端(首次需要):
supabase db push --linked- 登入 Cloudflare Dashboard
- 取得 Account ID(首頁右側欄或任一 Workers 頁面)
- 建立 API Token:My Profile → API Tokens → Create Token
- 使用 "Edit Cloudflare Workers" 模板
- 記錄
CLOUDFLARE_API_TOKEN
專案已內建
wrangler.toml,Nitro 使用cloudflare_modulepreset,部署時無需額外設定。
在 GitHub repo → Settings → Secrets and variables → Actions 新增:
| Secret 名稱 | 說明 | 取得方式 |
|---|---|---|
CLOUDFLARE_API_TOKEN |
Cloudflare API Token | Cloudflare Dashboard |
CLOUDFLARE_ACCOUNT_ID |
Cloudflare Account ID | Cloudflare Dashboard |
SUPABASE_URL |
Supabase Project URL | Supabase Dashboard |
SUPABASE_KEY |
Supabase anon key | Supabase Dashboard |
SUPABASE_SECRET_KEY |
Supabase service role key | Supabase Dashboard |
BETTER_AUTH_SECRET |
Auth 加密金鑰 | openssl rand -base64 32 |
NUXT_SESSION_PASSWORD |
Session 加密金鑰 | openssl rand -base64 32 |
NUXT_PUBLIC_SITE_URL |
Production URL | 你的網站網址 |
CI workflow 的 database job 會自動推送 Migration,需要額外設定:
| Secret 名稱 | 說明 | 取得方式 |
|---|---|---|
SUPABASE_ACCESS_TOKEN |
Supabase CLI Token | Supabase Dashboard → Access Tokens |
SUPABASE_DB_PASSWORD |
資料庫密碼 | 建立專案時設定的密碼 |
如果使用 OAuth 登入,依需要新增:
| Secret 名稱 | 說明 |
|---|---|
NUXT_OAUTH_GOOGLE_CLIENT_ID |
Google OAuth Client ID |
NUXT_OAUTH_GOOGLE_CLIENT_SECRET |
Google OAuth Client Secret |
NUXT_OAUTH_GITHUB_CLIENT_ID |
GitHub OAuth Client ID |
NUXT_OAUTH_GITHUB_CLIENT_SECRET |
GitHub OAuth Client Secret |
NUXT_OAUTH_LINE_CLIENT_ID |
LINE Login Channel ID |
NUXT_OAUTH_LINE_CLIENT_SECRET |
LINE Login Channel Secret |
如果使用 Sentry 錯誤監控:
| Secret 名稱 | 說明 |
|---|---|
NUXT_PUBLIC_SENTRY_DSN |
Sentry DSN |
SENTRY_AUTH_TOKEN |
Sentry Auth Token(用於 Source Map 上傳) |
SENTRY_ORG |
Sentry Organization Slug |
SENTRY_PROJECT |
Sentry Project Slug |
在 GitHub repo → Settings → Secrets and variables → Actions → Variables 新增:
| Variable 名稱 | 說明 | 用途 |
|---|---|---|
DEPLOY_URL |
部署後的網址(完整 URL 含 https://,不含結尾 /,例如 https://my-app.workers.dev) |
部署後的 Smoke Test |
專案提供 GitHub Actions workflow 範本,位於 scripts/templates/github/.github/workflows/。
建立新專案後,將範本複製到 .github/workflows/:
cp -r scripts/templates/github/.github .github觸發條件:所有 Pull Request + push 到 main
validate → lint → typecheck → unit tests
↓(僅 main branch)
e2e tests + database migration push
- validate:驗證 starter scaffold 結構
- e2e:Playwright 端對端測試(僅
mainbranch) - database:推送 Migration + 驗證 table owner + lint 資料庫
觸發條件:push 到 main + 手動觸發
checkout → install → build → wrangler deploy → smoke test
- 支援手動選擇
staging/production環境 - 使用 concurrency group 避免同時部署
- 部署後自動執行 Smoke Test(需設定
DEPLOY_URLvariable)
DEPLOY_URL 格式:完整 URL,包含
https://,不含結尾斜線。例如:https://my-app.workers.dev或https://my-domain.com注意:DEPLOY_URL是 GitHub Variable(Settings → Secrets and variables → Actions → Variables),不是 Secret。
重要:所有環境變數透過 GitHub Secrets 管理,禁止直接在 Cloudflare Dashboard 設定。這確保環境變數有版本控制,且團隊成員可透過 GitHub 統一管理。
- 所有檢查通過:
pnpm check - Build 成功:
pnpm build - 所有 GitHub Secrets 已設定(參考 Step 3)
- 資料庫 Migration 已推送:
supabase db push --linked - DNS 已設定(如使用自訂域名)
# 確認 build 成功
pnpm build
# 確認所有檢查通過
pnpm checkgit push origin mainGitHub Actions 會自動執行:
- CI workflow:lint → typecheck → test → database migration push
- Deploy workflow:build → wrangler deploy → smoke test
在 GitHub repo → Actions 頁面查看 workflow 執行狀態。
- 應用程式在 Production URL 正常載入
- 登入/註冊流程正常運作
- API 端點回應正確(
/api/v1/profiles/me) - Supabase 連線正常(資料可讀取)
- OAuth 登入正常(如有設定)
- Sentry 收到事件(如有設定)
# 檢查應用程式是否回應
curl -I https://your-app.workers.dev
# 檢查 API 健康狀態
curl https://your-app.workers.dev/api/v1/profiles/me# 查看部署歷史
npx wrangler deployments list
# 回滾到上一個版本
npx wrangler rollback# 檢查 migration 狀態
supabase migration list --linked
# 標記有問題的 migration 為 reverted
supabase migration repair --status reverted <version>資料庫回滾需要手動處理。建議在部署前備份:
pnpm db:backup
遇到問題?參考 TROUBLESHOOTING.md: