上 Production 完整 Checklist
2026-05-18 · 老闆「上 production 要做的事情整個 codebase 看一看」audit · 共 6 大類 / 30 個切換點
先看這裡 — TL;DR
3 個 critical 必須切才能收真錢:
restaurant_settings.newebpay_environment = 'production'(每家餐廳獨立)
- Supabase Functions env var
APNS_ENVIRONMENT = 'production'(全域,影響 wallet push 跟通用 push)
- Apple Wallet Pass
APPLE_PASS_CERTIFICATE 必須是 production cert(非 sandbox)且 UID 跟 pass.com.chefsmate.loyalty 一致
4 個 high 上 production 前要先測: ezPay 電子發票 / Stripe 訂閱 webhook / Resend email 寄件人網域 / TapPay(目前未實際接但已寫 code)
當前老闆兩家餐廳實際 DB 狀態:海邊小巫 newebpay+ezpay 都 sandbox,溪邊小巫沒設定 newebpay/ezpay。
A. 金流 — Per-restaurant DB 設定(每家餐廳獨立切)
A1. 藍新金流(newebpay) — 訂金 + 退款
使用的 column:restaurant_settings.newebpay_environment('sandbox' / 'production')
| 檔案 / 模組 | 當前 sandbox URL | 切 production 後 URL | 嚴重度 |
supabase/functions/newebpay-refund/index.ts (剛 ship v3 sandbox fallback) |
ccore.newebpay.com/API/CreditCard/Close |
core.newebpay.com/API/CreditCard/Close |
CRITICAL |
supabase/functions/newebpay-test-connection/index.ts |
ccore.newebpay.com/API/QueryTradeInfo |
core.newebpay.com/API/QueryTradeInfo |
MEDIUM |
web/booking/src/lib/newebpay/crypto.ts → getMpgUrl() |
ccore.newebpay.com/MPG/mpg_gateway |
core.newebpay.com/MPG/mpg_gateway |
CRITICAL |
web/booking/src/app/api/newebpay/deposit/create/route.ts |
用 settings.newebpay_environment 自動選 URL ✓ |
LOW |
web/booking/src/app/api/newebpay/create/route.ts(訂單付款) |
用 settings.newebpay_environment 自動選 URL ✓ |
LOW |
切換步驟
- 跟藍新申請正式 merchant 帳號(MerchantID + HashKey + HashIV)— 不是沙箱那組
- 跟藍新申請「退款 API 權限」(沙箱沒有此權限,production 預設也沒有,要另外開通)
- 給藍新 IP 白名單 — Supabase Edge Functions 是動態 IP,需要請藍新放寬或用 fixed IP gateway
- POS 後台「設定 → 付款方式」:把環境改成「正式環境」、填入 production 憑證
- DB:
UPDATE restaurant_settings SET newebpay_environment = 'production', newebpay_merchant_id = ..., newebpay_hash_key = ..., newebpay_hash_iv = ... WHERE restaurant_id = ...
- NotifyURL 確認(藍新後台填的 callback URL):
https://chefsmate.app/api/newebpay/deposit/notify 跟 /api/newebpay/notify
- 實測:刷 1 元退 1 元,藍新後台「信用卡請退款查詢」要看到紀錄
A2. ezPay 電子發票
使用的 column:restaurant_settings.ezpay_environment('sandbox' / 'production')
| 檔案 | sandbox | production |
supabase/functions/ezpay-invoice/index.ts |
cinv.ezpay.com.tw |
inv.ezpay.com.tw |
supabase/functions/ezpay-test-connection/index.ts |
cinv.ezpay.com.tw |
inv.ezpay.com.tw |
web/booking/src/lib/ezpay/crypto.ts(5 endpoints) |
cinv.ezpay.com.tw |
inv.ezpay.com.tw |
- 跟 ezPay 申請正式商店帳號(MerchantID + HashKey + HashIV)
- 確認餐廳的營業稅籍登記號碼 + 發票字軌已申請
- POS 後台「設定 → 電子發票」改成「正式環境」
- 實測:開立 1 元發票 → ezPay 後台應該看到
A3. TapPay 信用卡
使用的 env var(全域,非 per-restaurant):TAPPAY_ENVIRONMENT('sandbox' / 'production')
| 檔案 | sandbox URL | production URL |
supabase/functions/tappay-invoice/index.ts |
sandbox.tappaysdk.com |
prod.tappaysdk.com |
supabase/functions/tappay-payment/index.ts |
sandbox.tappaysdk.com |
prod.tappaysdk.com |
注意: web/booking/src/app/api/checkout/payments/[paymentId]/settle/route.ts 註解:
Adapter 選擇:現金用 CashAdapter;其他方法 MVP 暫時也先用 Cash(等 TapPay/Newebpay config 從 env 進來再切換)
— 即 TapPay 雖然有 edge function,但結帳流程實際上還沒接,目前所有非現金都走 Cash adapter。production 上線前需把 settle/reverse routes 真接 TapPay adapter。
B. APNs / Apple Wallet — Supabase Functions 全域 env var
B1. APNs 推播 — 老闆收訂位/取消/付款通知
env var:APNS_ENVIRONMENT = 'sandbox' / 'production'(Supabase Functions secret)
| 檔案 | 預設值 | 注意 |
send-push-notification |
預設 sandbox |
正式上線必切。production token 對 sandbox endpoint 會回 BadDeviceToken。 |
batch-update-wallet-passes |
預設 production |
OK |
update-wallet-pass |
預設 production |
OK |
create-wallet-offer |
預設 production |
OK |
send-user-silent-push |
自動 fallback(sandbox → production) |
不用切 — 每張 device token 兩個 endpoint 都試一次,BadDeviceToken 才 fail |
send-permission-invalidation |
自動 fallback(sandbox → production) |
不用切 |
陷阱(已踩過): debug build / TestFlight 拿到的 APNs token 是 sandbox token,App Store build 是 production token。
APNS_ENVIRONMENT 強制單一環境會打壞另一種 build 的 token → 已踩過。建議所有 push 函數都改自動 fallback 那條 path(像 send-user-silent-push 那樣)。
B2. Apple Wallet Pass 簽證 — 會員卡 / 集點卡
| env var | 用途 | 注意 |
APPLE_PASS_CERTIFICATE |
Pass signing certificate(PEM) |
必須是 production cert,UID 跟 pass.com.chefsmate.loyalty 完全一致。已踩過「UID 不對就靜默失敗」 |
APPLE_PASS_KEY |
對應 private key |
同上 |
APPLE_WWDR_CERTIFICATE |
Apple WWDR intermediate cert |
不分 sandbox/prod,但要用最新版(G4 in 2025+) |
APPLE_LOYALTY_PASS_CERTIFICATE
APPLE_LOYALTY_PASS_KEY |
替代 wallet cert(舊版?) |
確認哪份 cert 實際被用 |
APNS_TOPIC |
passTypeIdentifier(預設 pass.com.chefsmate.loyalty) |
必須跟 cert UID 一致 |
Team ID hardcoded 6T8L7SR6YF |
generate-wallet-pass/index.ts:273 |
切 team / 換開發者帳號要改 |
- 確認 Apple Developer Portal 有效的 Pass Type ID(production)
- 下載 production cert(.p12),轉 PEM 上 Supabase Functions secrets
- WWDR cert 也要(
g4.cer 最新)
- 實測:發一張卡 → iPhone 加入 Wallet → 更新一次 → 卡片要自動 reload
C. OAuth(Apple / Google / LINE) — Supabase Functions env var
影響 functions:oauth-link-merge / line-link-merge
| env var | 用途 | 備註 |
APPLE_SERVICES_ID |
Sign In with Apple 的 Services ID |
Apple Developer Portal 設定的 Service ID(non-app) |
APPLE_SIGNIN_KEY_ID |
Sign In with Apple JWT key ID |
同 Apple Developer Portal Keys |
APPLE_SIGNIN_PRIVATE_KEY |
Sign In with Apple JWT private key(PEM) |
10 個月會過期,production 上線要排程 rotate |
APPLE_TEAM_ID |
Apple team ID |
跟 hardcoded 6T8L7SR6YF 必須一致 |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
Google OAuth |
Google Cloud Console 建立 production OAuth credentials;callback URL 加 chefsmate.app domain |
LINE_CHANNEL_ID / LINE_CHANNEL_SECRET |
LINE Login |
LINE Developers Console 設 production channel + callback URL |
GOOGLE_WALLET_ISSUER_ID
GOOGLE_WALLET_SERVICE_ACCOUNT_JSON |
Google Wallet pass |
申請 Google Pay & Wallet Console issuer account |
CWA_API_KEY |
中央氣象局 API 金鑰 |
免費申請,production 直接用就好 |
- callback URL 統一用 production domain(
chefsmate.app 或自訂 domain)
- Apple Sign In key 設提醒 9 個月後 rotate(避免過期突然全員登不進)
D. Stripe 訂閱(餐廳老闆訂閱 ChefsMate)
| env var | 當前範例 | production |
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY |
pk_test_xxx(來自 .env.local.example) |
pk_live_xxx |
STRIPE_SECRET_KEY |
sk_test_xxx |
sk_live_xxx |
STRIPE_WEBHOOK_SECRET |
whsec_test_xxx |
whsec_live_xxx(Stripe Dashboard → Webhooks → 對應 production endpoint 才會生) |
- Stripe Dashboard 切到 Live mode 建 webhook endpoint:
https://chefsmate.app/api/billing/webhook 跟 /api/stripe/webhook
- Vercel env 把 3 個 Stripe key 都改 live
- 確認所有訂閱方案的
stripe_price_id_monthly / stripe_price_id_yearly(在 subscription_plans 表)是 live mode 的 price ID,不是 test mode
- 實測:用真信用卡訂閱 1 個月最便宜方案 → webhook 應該收到
checkout.session.completed
E. Hardcoded chefsmate.app URLs
如果未來換 domain(例:chefsmate.tw),這些都要改:
| 檔案 | 用途 |
supabase/functions/batch-update-wallet-passes/index.ts (×4) | Wallet pass 內的連結(checkin / menu / booking / stamp image) |
supabase/functions/send-deposit-received/index.ts BASE_URL | email 內連結 |
supabase/functions/send-reservation-confirmed/index.ts BASE_URL | email 內連結 |
supabase/functions/send-modification-squeeze-result/index.ts | email 連客人訂位頁 |
supabase/functions/send-cancellation-email/index.ts FROM_EMAIL | 寄件人 noreply@chefsmate.app |
supabase/functions/send-staff-invitation-email/index.ts | 員工邀請信內連結 + APP_STORE_URL_PLACEHOLDER |
supabase/functions/update-wallet-pass/index.ts | stamp image URL |
supabase/functions/generate-wallet-pass/index.ts | Pass body 內所有 URL |
Apps/ChefsMatePOS/.../*.swift (10+ 處) | POS 打 web API base URL |
- 所有 edge function 改用
Deno.env.get('PUBLIC_WEB_BASE') 取代 hardcoded(目前只有部分檔案有)
- POS Swift 改抽
Config.apiBaseUrl(目前散落各 view 自寫 https://chefsmate.app/...)
APP_STORE_URL_PLACEHOLDER(在 staff invitation 信)上 App Store 後改成真實連結
F. iOS App build / TestFlight / App Store 注意事項
| 項目 | 當前狀態 | 切 production 要做 |
| Bundle ID |
com.ciro.ChefsMate 等 |
確認 Apple Developer Portal app ID + Provisioning Profile |
APNS_TOPIC (push 用的 Bundle ID) |
預設 com.ciro.ChefsMate |
對應每個 app 的 Bundle ID(POS / Customer / Booking 各別) |
| APNs Auth Key (.p8) |
APNS_KEY_P8 env var |
跟 Apple Developer Portal 一致的 production key |
| Sign in with Apple — Service ID |
APPLE_SERVICES_ID |
確認對應 production domain |
| App Universal Links |
/.well-known/apple-app-site-association(vercel.json 有設 header) |
內容要列 production app IDs |
- POS / Customer / Booking 三個 app 各別在 App Store Connect 建 production listing
- TestFlight build 跟 App Store build 用同個 Bundle ID 但簽證不同 → APNs token 環境也不同
- 送審前確認所有 hardcoded URL 已切 production domain
G. 切換 Production 的完整 deploy 步驟(建議順序)
- Domain:確認
chefsmate.app(或新 domain)DNS 指向 Vercel
- Vercel Env:Production scope 設 Stripe live keys、Resend API key、Supabase URL/key
- Supabase Functions secrets:
APNS_ENVIRONMENT=production
APNS_KEY_P8/ID/TEAM_ID = production
APPLE_PASS_* = production wallet cert
- OAuth secrets(Apple / Google / LINE)= production app credentials
RESEND_API_KEY = live key, FROM_EMAIL = 已驗證網域
PUBLIC_WEB_BASE=https://chefsmate.app
- 金流(per-restaurant):每家上線餐廳分別在 POS 後台填 production 憑證 + 切環境
- Stripe Dashboard:Live mode webhook endpoint + 建 live mode prices,寫進
subscription_plans 表
- iOS app:Archive → App Store Connect 上傳;同時更新 TestFlight
- Wallet:確認 production cert UID 跟
pass.com.chefsmate.loyalty 一致(已踩過陷阱)
- 實測 smoke test:
- 客人線上付訂金 1 元 → 收 push → POS 看到「訂金已收」
- 客人取消 → POS 收 push + UI 自動刷新
- POS 同意退款 → 藍新後台「信用卡請退款查詢」看到記錄(這次沙箱看不到,production 才能驗)
- 餐廳老闆訂閱 1 個月最便宜方案 → webhook + 訂閱狀態
- 客人加 Wallet 集點卡 → 更新一次 → 卡片 reload
已知陷阱(memory 已記):
- Vercel Hobby plan cron 規定每天最多 1 次 → 已有
vercel.json 只設 1 cron(/api/credit/cron/expire-overdue 03:00),不要加
- Apple Wallet UID 跟 passTypeIdentifier 不對會「靜默失敗」
- Apple Sign In key 9 個月過期
- Resend domain 沒驗證會被當垃圾信
ChefsMate Audit · 2026-05-18 · 老闆 dogfood 觸發