Supabase Edge Functions Secrets、環境與部署實戰 | Supabase 完整教學
上一篇《Edge Functions 入門》我們寫出並在本機跑通了第一個以 Deno 為底層的無伺服器函式,但它還沒真正上線,金鑰也還躺在本機的
.env裡。這一篇處理兩件把函式「推上正式環境」必修的事:Secrets 與環境變數——用supabase secrets set安全管理正式環境金鑰、用Deno.env.get()在函式裡讀取,並認識平台內建自動注入的SUPABASE_URL、SUPABASE_SERVICE_ROLE_KEY;以及部署——用supabase functions deploy把函式送上全球邊緣網路、--no-verify-jwt做公開端點、import map/deno.json鎖定套件版本。學完這篇,你的第一個 Edge Function 就能安全地對外服務。
前言
一句話定義本篇主題:Secrets 與部署,是把 Edge Function 從「本機能跑」推進到「正式環境安全對外服務」的最後一哩路——你用 supabase secrets set 把私密金鑰交給正式環境、用 Deno.env.get() 在函式裡讀取、用 supabase functions deploy 把程式碼推上 Supabase 的全球邊緣節點,並在過程中管好 CORS、JWT 驗證與套件版本。
先接住上一篇的脈絡。《Edge Functions 入門》裡我們的 get-weather 函式把 WEATHER_API_KEY 用 Deno.env.get() 讀出來、藏在伺服器端不外洩——這在本機是靠 supabase/functions/.env 檔案餵進去的。但當你要把函式部署到正式環境時,.env 檔案並不會跟著上去(它也絕不該被 commit 進 git),這時就需要一套「正式環境的機密管理機制」,這正是本篇的第一個主角:Secrets。
第二個主角是部署本身。上一篇函式只跑在本機的 supabase functions serve;要讓世界各地的使用者、或 Stripe 這類外部服務打得到它,你得執行 supabase functions deploy 把它推上 Supabase 的全球邊緣網路,並決定它是「需要登入才能呼叫」還是「公開端點」。
用一個生活類比:如果說上一篇是「在自家廚房試做出一道菜」,這一篇就是「把這道菜端上正式營業的餐廳」。Secrets 就是那些只有店家能碰的保險箱鑰匙(付費 API 金鑰)——本機試做時鑰匙放在自家抽屜(.env),正式營業時得交給餐廳的保險箱系統(supabase secrets)妥善保管,服務生(函式)需要時再去保險箱取。而 deploy 就是開店營業這個動作:把菜單與作法正式登記、對外掛牌,客人才點得到。
本篇你會學到:
- Secrets 管理:
supabase secrets set/list/unset管理正式環境機密、本機.env的自動載入、Deno.env.get()讀取,以及「改 Secret 不必重新 deploy」的關鍵特性 - 內建環境變數:平台自動注入的
SUPABASE_URL、SUPABASE_SERVICE_ROLE_KEY、SUPABASE_ANON_KEY等,以及命名規則限制(不能用SUPABASE_前綴) - 在函式內建 client:用
service_role key建立繞過 RLS 的管理員客戶端、用anon key+ 使用者 JWT 建立遵守 RLS 的客戶端,兩者何時該用哪一個 - 部署與上線:
supabase functions deploy、--no-verify-jwt做公開端點、CORS 設定、import map/deno.json鎖定套件版本
本篇聚焦「部署與機密」,入門觀念(
Deno.serve、functions new、functions.invoke)請見上一篇《Edge Functions 入門》,這裡不再重複。
核心概念
要把 Secrets 與部署做對,先建立三張心智圖:一張是「Secrets 與環境變數的三個來源」,一張是「內建自動注入的變數有哪些、怎麼用」,一張是「從本機到正式環境的部署流程」。
一、環境變數的三個來源
在 Edge Function 裡,你透過 Deno.env.get('KEY') 讀取環境變數。但這些變數其實來自三個不同的地方,搞清楚來源才不會踩雷:
| 來源 | 怎麼設定 | 本機 vs 正式 | 典型變數 |
|---|---|---|---|
| 平台內建注入 | 無需設定,自動存在 | 兩者皆有 | SUPABASE_URL、SUPABASE_SERVICE_ROLE_KEY、SUPABASE_ANON_KEY |
本機 .env 檔 | 放進 supabase/functions/.env 或 --env-file 指定 | 只在本機 | WEATHER_API_KEY、OPENAI_API_KEY(開發用值) |
| 正式環境 Secrets | supabase secrets set KEY=val | 只在正式環境 | WEATHER_API_KEY、OPENAI_API_KEY(正式值) |
關鍵在於同一個 Deno.env.get('OPENAI_API_KEY'),在本機讀到的是 .env 裡的開發用金鑰,在正式環境讀到的是你用 supabase secrets set 設定的正式金鑰。你的程式碼完全不用改——這就是環境變數抽象的好處:程式碼一份,機密隨環境切換。
一張圖看懂:
你的函式程式碼(一份)
Deno.env.get('OPENAI_API_KEY')
│
┌───────────────┴───────────────┐
▼ ▼
[本機 supabase functions serve] [正式 supabase functions deploy]
讀 supabase/functions/.env 讀 supabase secrets(正式機密庫)
→ sk-test-xxxx(開發用) → sk-live-xxxx(正式用)
二、內建自動注入的環境變數
Supabase 在函式執行環境裡自動注入一批變數,你不必設定就能直接用。最常用的幾個:
| 變數名稱 | 說明 | 常見用途 |
|---|---|---|
SUPABASE_URL | 專案的 API 閘道位址 | 建立 Supabase 客戶端的第一個參數 |
SUPABASE_ANON_KEY | 匿名(可公開)金鑰 | 建立「遵守 RLS」的使用者範圍客戶端 |
SUPABASE_SERVICE_ROLE_KEY | 服務角色金鑰(可繞過 RLS) | 建立「繞過 RLS」的管理員客戶端 |
SUPABASE_DB_URL | 直連 PostgreSQL 的連線字串 | 需要原生 SQL/連線池時 |
兩個重點:
- 這些變數在本機(
supabase start後)與正式環境(deploy 後)都自動存在,所以你在函式裡寫Deno.env.get('SUPABASE_URL')兩邊都能拿到值,不必手動設定。 - 命名保留:你自訂的 Secrets 不能以
SUPABASE_為前綴(這個前綴保留給平台內建變數)。所以你的金鑰要叫OPENAI_API_KEY、STRIPE_SECRET_KEY,不能叫SUPABASE_OPENAI_KEY。
SUPABASE_SERVICE_ROLE_KEY是本篇最需要謹慎對待的變數:它能繞過所有 RLS,等於資料庫的萬能鑰匙。它自動注入在伺服器端的函式環境裡是安全的(前端拿不到),但你絕不能把它回傳給前端、或寫進任何客戶端可見的地方。
三、從本機到正式環境的部署流程
把函式推上線的完整流程,可以濃縮成這張圖:
[1] 本機開發完成
supabase functions serve(讀 .env 測試)
│
▼
[2] 連結遠端專案(只需一次)
supabase login
supabase link --project-ref <PROJECT_ID>
│
▼
[3] 設定正式環境 Secrets
supabase secrets set OPENAI_API_KEY=sk-live-xxxx
supabase secrets list(確認名稱已存在)
│
▼
[4] 部署函式
supabase functions deploy get-weather
(公開端點加 --no-verify-jwt)
│
▼
[5] 對外可達
https://<PROJECT_ID>.supabase.co/functions/v1/get-weather
關鍵術語先建立:
supabase link:把本機專案「連結」到 Supabase 上的遠端專案(用 Dashboard 取得的PROJECT_ID),之後的deploy、secrets set才知道要操作哪個專案。通常只需做一次。supabase secrets:管理正式環境機密的指令組(set/list/unset),與本機.env完全獨立。supabase functions deploy:把函式程式碼打包、上傳、部署到全球邊緣節點。--no-verify-jwt:部署旗標,關閉平台層的 JWT 驗證,讓函式成為公開端點(Webhook、公開 API 必用)。
實作範例
下面用可直接執行的指令與 TypeScript,走一遍「設定 Secrets → 在函式內建 client → 處理 CORS → 部署上線」的完整流程。假設你已完成上一篇的本機開發,並已安裝 Supabase CLI。以下 API 名稱與行為細節請以官方文件為準。
一、本機環境變數:.env 與 Deno.env.get
先看本機。上一篇提到金鑰要用 Deno.env.get() 讀、不寫死。本機開發時,這些值來自 supabase/functions/.env(supabase functions serve 會自動載入這個檔案):
# supabase/functions/.env
# 這些是「本機開發用」的值,絕不 commit 進 git
WEATHER_API_KEY=dev_weather_key_1234
OPENAI_API_KEY=sk-test-abcdefg
STRIPE_SECRET_KEY=sk_test_xxxxxxxx
第一件事:把 .env 加進 .gitignore,這是安全的底線:
# 確保 .env 系列檔案永遠不會被 commit
echo "supabase/functions/.env" >> .gitignore
echo ".env" >> .gitignore
echo ".env.local" >> .gitignore
在函式裡讀取——不論本機或正式環境,程式碼都一樣:
// supabase/functions/get-weather/index.ts
// 在 handler「外部」讀取:模組載入時執行一次即可
const WEATHER_API_KEY = Deno.env.get("WEATHER_API_KEY")
Deno.serve(async (req: Request) => {
// 防禦性檢查:缺變數時給出明確錯誤,而不是等到 fetch 才神秘失敗
if (!WEATHER_API_KEY) {
console.error("WEATHER_API_KEY 未設定")
return Response.json({ error: "伺服器設定錯誤" }, { status: 500 })
}
const { city } = await req.json()
const apiRes = await fetch(
`https://api.example-weather.com/v1/current` +
`?key=${WEATHER_API_KEY}&q=${encodeURIComponent(city)}`,
)
const raw = await apiRes.json()
return Response.json({ city, tempC: raw.current?.temp_c })
})
啟動本機開發伺服器時,.env 會被自動載入;也可以用 --env-file 指定其他檔案:
# 自動載入 supabase/functions/.env
supabase functions serve get-weather
# 或明確指定環境變數檔(例如切換到另一組本機設定)
supabase functions serve get-weather --env-file ./supabase/functions/.env.local
二、正式環境 Secrets:secrets set / list / unset
本機驗證通過後,換設定正式環境的機密。這裡用的是 supabase secrets,與本機 .env 是兩套獨立系統。先確認已連結遠端專案:
# 登入(開瀏覽器授權,只需一次)
supabase login
# 連結到遠端專案(PROJECT_ID 從 Dashboard → Project Settings 取得)
supabase link --project-ref your-project-id
接著設定 Secrets:
# 設定單一 Secret(KEY=value 格式)
supabase secrets set OPENAI_API_KEY=sk-live-realproductionkey
# 一次設定多個
supabase secrets set STRIPE_SECRET_KEY=sk_live_xxx WEATHER_API_KEY=prod_weather_key
# 從 .env 檔批次匯入(注意:這是正式值的檔案,同樣不該進 git)
supabase secrets set --env-file ./.env.production
# 列出所有已設定的 Secret「名稱」(基於安全,不會顯示值)
supabase secrets list
# 刪除某個 Secret
supabase secrets unset WEATHER_API_KEY
supabase secrets list 的輸出只會列名稱與一段 digest,不會印出實際的值——這是刻意的安全設計,避免金鑰在終端機或 CI 日誌裡外洩。
一個容易被忽略、但很好用的特性:設定或更新 Secret 後,通常不需要重新 deploy 函式,變數就會對之後的請求生效。因為 Deno.env.get() 讀的是「執行當下環境裡的值」,不是「部署當下打包進去的值」。所以你可以事後輪替金鑰而完全不動程式碼。
三、在函式內建立 Supabase 客戶端:service_role vs anon
這是本篇最核心的實作。在 Edge Function 裡,你常需要讀寫自己的 Postgres 資料庫。因為平台自動注入了 SUPABASE_URL 與各種 key,你可以直接用它們建立客戶端。關鍵是選對 key,這決定了「要不要遵守 RLS」。
情境 A:管理員客戶端(用 service_role key,繞過 RLS)
適用於明確需要跨使用者權限的後台作業,例如排程彙總、Webhook 寫入:
// supabase/functions/admin-report/index.ts
import { createClient } from "npm:@supabase/supabase-js@2"
// 用 service_role key 建立管理員客戶端 → 繞過所有 RLS
// 放在 handler 外部,只建立一次
const supabaseAdmin = createClient(
Deno.env.get("SUPABASE_URL")!, // 內建注入,不必自己設
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!, // 內建注入,可繞過 RLS
{
auth: {
autoRefreshToken: false, // 伺服器端不需要自動更新 token
persistSession: false, // 也不需要保存 session
},
},
)
Deno.serve(async (_req: Request) => {
// 這個查詢會「看到所有使用者的資料」,因為 service_role 繞過 RLS
const { data, error } = await supabaseAdmin
.from("orders")
.select("id, user_id, amount")
.gte("created_at", new Date(Date.now() - 86400000).toISOString())
if (error) {
return Response.json({ error: error.message }, { status: 500 })
}
return Response.json({ count: data.length, orders: data })
})
情境 B:使用者範圍客戶端(用 anon key + 使用者 JWT,遵守 RLS)
適用於「代表某位登入使用者操作」,讓 RLS 自動把資料限縮在該使用者權限內:
// supabase/functions/my-documents/index.ts
import { createClient } from "npm:@supabase/supabase-js@2"
Deno.serve(async (req: Request) => {
// 從請求取出呼叫者的 JWT(supabase.functions.invoke 會自動帶上)
const authHeader = req.headers.get("Authorization")
if (!authHeader) {
return Response.json({ error: "Unauthorized" }, { status: 401 })
}
// 用 anon key 建立客戶端,並把呼叫者的 Authorization 標頭轉傳進去
// → 查詢會以「該登入使用者」身分執行,RLS 完整生效
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_ANON_KEY")!, // 注意:是 anon key 不是 service_role
{
global: { headers: { Authorization: authHeader } },
auth: { autoRefreshToken: false, persistSession: false },
},
)
// RLS 生效:只會回傳「這位使用者有權限看到」的 documents
const { data, error } = await supabase.from("documents").select("*")
if (error) {
return Response.json({ error: error.message }, { status: 400 })
}
return Response.json({ documents: data })
})
一句話記住兩者的分野:要遵守呼叫者權限就用 anon key + 使用者 JWT(RLS 生效);明確要管理員權限跨使用者操作才用 service_role key(繞過 RLS)。 這個選擇攸關資料安全,下一節會再強調它的坑。
四、CORS:部署前務必確認
上一篇談過 CORS 的原理,這裡補上部署前的檢查點。只要函式會被瀏覽器呼叫,就必須處理 OPTIONS preflight 並在每個回應帶上 CORS 標頭,否則部署後從網頁呼叫就會被擋。把 CORS 標頭抽成共用模組:
// supabase/functions/_shared/cors.ts
// 以 _ 開頭的資料夾不會被部署為獨立函式,適合放共用模組
export const corsHeaders = {
"Access-Control-Allow-Origin": "*", // 正式環境建議改成你的具體網域
"Access-Control-Allow-Headers":
"authorization, x-client-info, apikey, content-type",
"Access-Control-Allow-Methods": "POST, OPTIONS",
}
在函式裡套用(成功、錯誤、preflight 三處都要帶):
// supabase/functions/my-documents/index.ts(加上 CORS)
import { createClient } from "npm:@supabase/supabase-js@2"
import { corsHeaders } from "../_shared/cors.ts"
Deno.serve(async (req: Request) => {
// 1) 先回應瀏覽器的 preflight
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders })
}
const authHeader = req.headers.get("Authorization")
if (!authHeader) {
// 2) 錯誤回應也要帶 CORS 標頭
return Response.json(
{ error: "Unauthorized" },
{ status: 401, headers: corsHeaders },
)
}
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_ANON_KEY")!,
{ global: { headers: { Authorization: authHeader } } },
)
const { data, error } = await supabase.from("documents").select("*")
// 3) 成功回應同樣帶上 CORS 標頭
return Response.json(
error ? { error: error.message } : { documents: data },
{ status: error ? 400 : 200, headers: corsHeaders },
)
})
五、部署上線:functions deploy 與 –no-verify-jwt
一切就緒,執行部署。supabase functions deploy 會把函式打包上傳到全球邊緣節點:
# 部署單一函式
supabase functions deploy my-documents
# 部署所有函式(一次上傳 supabase/functions/ 下全部)
supabase functions deploy
# 部署為「公開端點」:關閉平台層 JWT 驗證(Webhook、公開 API 用)
supabase functions deploy stripe-webhook --no-verify-jwt
部署完成後,函式的正式 URL 是:
https://<PROJECT_ID>.supabase.co/functions/v1/<function-name>
關於 --no-verify-jwt 的關鍵理解:Edge Function 預設要求請求帶有效 JWT(平台層在函式執行前就會擋掉沒帶 token 的請求)。這對「只給登入使用者呼叫」的函式是好事;但對 Stripe Webhook 這種「由外部服務主動打進來、根本沒有使用者 JWT」的端點,你就必須用 --no-verify-jwt 關掉這道驗證,改在函式內部自己驗證(例如驗 Stripe 簽章)。除了旗標,也可以在 supabase/config.toml 針對個別函式設定:
# supabase/config.toml
[functions.stripe-webhook]
verify_jwt = false # 等同 --no-verify-jwt,但寫在設定檔更明確、可版控
[functions.my-documents]
verify_jwt = true # 預設值,需要登入才能呼叫
六、鎖定套件版本:import map 與 deno.json
最後一個部署品質關卡:版本鎖定。上一篇我們用 npm:@supabase/supabase-js@2 這種寫法匯入套件,@2 是「2.x 最新」的浮動版本——正式環境為了可重現、避免某天套件更新突然壞掉,建議把版本釘死。Deno 現在推薦用 deno.json 的 imports 欄位集中管理:
// supabase/functions/deno.json
{
"imports": {
"@supabase/supabase-js": "npm:@supabase/supabase-js@2.49.0",
"zod": "npm:zod@3.23.8",
"hono": "jsr:@hono/hono@4.6.0"
}
}
有了它,函式裡就能用「裸名稱」匯入,版本統一由 deno.json 控制:
// 不再寫死版本號,改用 deno.json 對應的裸名稱
import { createClient } from "@supabase/supabase-js"
import { z } from "zod"
部署時,若你的映射檔不叫 deno.json(例如沿用舊的 import_map.json),可用旗標明確指定:
# 用 deno.json(放在函式資料夾或 supabase/functions/ 下)通常會自動偵測
supabase functions deploy my-documents
# 或明確指定 import map 檔
supabase functions deploy my-documents --import-map ./supabase/functions/import_map.json
版本鎖定的好處是可重現的部署:今天 deploy 和三個月後 deploy 用的是同一套套件版本,不會因為上游套件的破壞性更新而在正式環境突然出包。
常見錯誤與最佳實踐
坑一:把 service_role client 當 anon client 用來處理一般使用者請求。
這是本篇最危險的坑。用 SUPABASE_SERVICE_ROLE_KEY 建立的客戶端繞過所有 RLS,如果你拿它來處理「代表某位使用者」的查詢,卻忘了手動加 .eq('user_id', user.id) 這類過濾,使用者就能讀到別人的資料——你等於親手廢掉 RLS 這道防線。正確做法:處理使用者請求時,用 SUPABASE_ANON_KEY + 呼叫者的 JWT(global.headers.Authorization)建立客戶端,讓 RLS 自動限縮權限;只有在明確需要跨使用者的管理員作業(排程、Webhook、後台)才用 service_role,而且務必想清楚「這真的需要繞過 RLS 嗎」。
坑二:把 .env(或正式金鑰)commit 進 git。
本機的 supabase/functions/.env、批次匯入用的 .env.production 一旦被 commit,金鑰就永久留在 git 歷史裡,即使之後刪除也已外洩。正確做法:第一時間把 .env、.env.local、.env.production、supabase/functions/.env 全加進 .gitignore;正式金鑰只透過 supabase secrets set 進入正式環境,本機用開發用的假金鑰或測試金鑰;若不慎 commit 過,除了刪檔還要立刻輪替該金鑰(改 Secret 不必重新 deploy,成本很低)。
坑三:以為 .env 會跟著 deploy 上正式環境。
本機 .env 只在 supabase functions serve 時載入,不會隨 supabase functions deploy 上去;部署後函式讀不到你以為存在的變數,Deno.env.get() 回傳 undefined,於是在正式環境神秘失敗。正確做法:正式環境的機密一律用 supabase secrets set 設定,部署後用 supabase secrets list 確認名稱都在;在函式裡對關鍵變數做防禦性檢查(缺少時回明確的 500 而非讓後續 fetch 爆掉),並到 Dashboard 函式日誌確認。
坑四:CORS 沒處理,部署後從瀏覽器呼叫全被擋。
本機用 curl 測都正常,一放到網頁就 CORS error——因為沒回應 OPTIONS preflight,或回應沒帶 Access-Control-Allow-* 標頭。「curl 通、瀏覽器不通」是典型症狀。正確做法:只要函式會被瀏覽器直接呼叫,就在 handler 開頭處理 req.method === 'OPTIONS' 回帶 CORS 標頭的回應,並讓成功與錯誤的每個回應都帶上同一組標頭(抽成 _shared/cors.ts);正式環境把 Access-Control-Allow-Origin 從 * 收斂成你的具體網域。
坑五:公開端點忘了 --no-verify-jwt,Webhook 一直被平台擋在門外。
Edge Function 預設要求有效 JWT,Stripe/GitHub 這類 Webhook 由外部服務主動打進來、根本帶不出使用者 token,於是被平台層直接擋掉,你在函式日誌裡連請求都看不到。正確做法:公開端點(Webhook、免登入 API)部署時加 --no-verify-jwt,或在 config.toml 設 verify_jwt = false;關掉平台驗證後,務必在函式內部自己做驗證(如驗 Stripe 簽章、檢查密鑰),別讓端點裸奔。
坑六:套件版本用浮動 tag(@2、@latest),正式環境某天突然壞掉。
npm:@supabase/supabase-js@2 會抓 2.x 最新版,上游某次更新引入破壞性變更時,你的函式會在沒改任何程式碼的情況下出包,且難以重現。正確做法:正式環境用 deno.json 的 imports 把版本釘到精確號(如 @2.49.0),追求可重現的部署;升級時明確改版本號、在本機驗過再 deploy。
最佳實踐總結:
- 選對 key:使用者請求用
anon+ JWT(RLS 生效);管理員作業才用service_role(繞過 RLS),且絕不外洩 .env一律進.gitignore:正式機密只走supabase secrets set,不慎外洩立刻輪替- 分清環境:本機讀
.env、正式讀 Secrets,同一份程式碼靠Deno.env.get()抽象切換 - 善用「改 Secret 不必重 deploy」:金鑰輪替成本很低,程式碼一份到底
- CORS 三處都帶:preflight、成功、錯誤回應都帶 CORS 標頭,正式環境收斂 origin
- 公開端點顯式
--no-verify-jwt:關平台驗證後在函式內自己驗 - 版本釘死:
deno.json精確版本號,換取可重現的正式部署 - 不確定的 API 以官方為準:
supabase secrets、functions deploy、旗標與內建變數名稱都可能調整,正式決策前查官方文件
小結
這是 Supabase 系列教學的第 032 篇,緊接 SB-4 運算與 AI 的開篇。上一篇《Edge Functions 入門》我們認識了以 Deno 為底層的 Edge Functions,寫出並在本機跑通第一個函式;這一篇補上把它推上正式環境必修的兩件事——Secrets 管理與部署。
核心觀念濃縮成一句話:同一份函式程式碼靠 Deno.env.get() 讀環境變數,本機值來自 .env、正式值來自 supabase secrets set(改 Secret 不必重新 deploy);函式內建 Supabase 客戶端時,處理使用者請求用 anon key + JWT 讓 RLS 生效、管理員作業才用可繞過 RLS 的 service_role key;用 supabase functions deploy 把函式送上全球邊緣網路,公開端點加 --no-verify-jwt,並用 deno.json 釘死套件版本、記得處理 CORS。
- 三個變數來源:平台內建注入(
SUPABASE_URL/SERVICE_ROLE_KEY)、本機.env、正式supabase secrets - 建 client 選 key:
anon+ 使用者 JWT(遵守 RLS)vsservice_role(繞過 RLS) - 部署三件事:
functions deploy上線、--no-verify-jwt做公開端點、deno.json鎖版本 - 安全底線:
.env進.gitignore、service_role絕不外洩、CORS 每個回應都帶
你的第一個 Edge Function 現在能安全地對外服務了。但很多真實需求是「回應使用者之後、還要在背景繼續做事」(寄信、寫日誌),或「不靠使用者觸發、而是定時自動執行」(每日報表、清理過期資料)。下一篇《Background Tasks 與 Cron》,我們就來處理這兩類需求:用 EdgeRuntime.waitUntil 做回應後的背景任務,用 pg_cron + pg_net 讓資料庫定時呼叫你的函式。運算篇章繼續前進,下一篇見。