Edge Functions กับฐานข้อมูล
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”ใน Edge Function คุณ query ฐานข้อมูลด้วย supabase-js ได้เหมือนโค้ดฝั่ง client เป๊ะ ๆ แต่ key ที่ใช้สร้าง client นั้นต่างหากที่ตัดสินว่า Row Level Security จะทำงานหรือถูก bypass ไปเลย
client เดียวกัน แต่ key ต่างกันคนละเรื่อง
หัวข้อที่มีชื่อว่า “client เดียวกัน แต่ key ต่างกันคนละเรื่อง”Edge Function เป็นแค่ TypeScript ฝั่งเซิร์ฟเวอร์ ไม่มีอะไรห้าม function สร้าง supabase-js client แล้ว query แบบเดียวกับที่ browser ทำ
import { createClient } from 'jsr:@supabase/supabase-js@2'
const authHeader = req.headers.get('Authorization')!
const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_ANON_KEY')!, { global: { headers: { Authorization: authHeader } } },)การ forward header Authorization ของผู้เรียกเอง ที่เป็น token เดียวกับที่ supabase.functions.invoke() แนบให้อัตโนมัติตามที่บทก่อนหน้าพูดถึง หมายความว่าทุก query ที่ client นี้รันจะถูกจำกัดขอบเขตตาม user คนนั้น Row Level Security ทำงานเหมือนกับที่ client เรียก Postgres ตรง ๆ ทุกประการ function ไม่ได้สิทธิ์เพิ่มขึ้นเลย เพียงแค่เพิ่ม logic ฝั่งเซิร์ฟเวอร์เข้าไปคลุมสิทธิ์ที่ user มีอยู่แล้ว
service_role key bypass RLS ทั้งหมด
หัวข้อที่มีชื่อว่า “service_role key bypass RLS ทั้งหมด”ทางเลือกอีกแบบคือสร้าง client ด้วย service_role key แทนที่จะใช้ anon key พร้อม forward token
const supabaseAdmin = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,)query ที่ทำผ่าน client นี้จะbypass RLS ทั้งหมด ทุก row ในทุก table มองเห็นและเขียนได้หมด ไม่สนใจ policy ใด ๆ นี่เป็น pattern ที่ถูกต้องและใช้กันทั่วไป เพราะ Edge Function เป็นโค้ดฝั่งเซิร์ฟเวอร์ที่เชื่อถือได้ ต่างจาก browser ที่ user ของตัวเองสามารถตรวจดูหรือแก้ไขได้ แต่มาพร้อมความรับผิดชอบที่เปลี่ยนไปจริง ๆ เมื่อ RLS ไม่ได้อยู่ในสมการแล้ว ฐานข้อมูลจะไม่ตรวจสอบอะไรแทน client นี้อีกต่อไป การควบคุมสิทธิ์ที่จำเป็นต้องเขียนให้ถูกต้องเองในโค้ดของ function
ตัวอย่างจริง รายงานสำหรับ admin เท่านั้น
หัวข้อที่มีชื่อว่า “ตัวอย่างจริง รายงานสำหรับ admin เท่านั้น”สมมติว่าคุณต้องการ endpoint ที่คืนรายงานสรุปยอด เช่น revenue รวมของลูกค้าทั้งหมด ซึ่ง RLS policy ของ user ทั่วไปไม่ควรอนุญาตให้เห็นเลย RLS เลือก “อนุญาต query สรุปนี้เฉพาะกรณีนี้” ไม่ได้ RLS ทำได้แค่ให้เห็น row หรือไม่ให้เห็นเท่านั้น Edge Function จึงเป็นที่ที่เหมาะสมสำหรับบังคับข้อยกเว้นนี้
Deno.serve(async (req) => { const authHeader = req.headers.get('Authorization')!
// Check the caller's identity and role using their own forwarded token const supabase = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_ANON_KEY')!, { global: { headers: { Authorization: authHeader } } }, ) const { data: { user } } = await supabase.auth.getUser()
const { data: profile } = await supabase .from('profiles') .select('is_admin') .eq('id', user?.id) .single()
if (!profile?.is_admin) { return new Response('Forbidden', { status: 403 }) }
// Only after that check passes, query freely with service_role const supabaseAdmin = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!, ) const { data: report } = await supabaseAdmin.rpc('total_revenue_report')
return new Response(JSON.stringify(report), { headers: { 'Content-Type': 'application/json' }, })})ตัวฟังก์ชันเองคือ trust boundary ในตัวอย่างนี้ ไม่ใช่ RLS ฟังก์ชันตรวจสอบว่าใครเรียกด้วย token ของ user คนนั้นเอง แล้วค่อยหยิบ service_role มาดึงข้อมูลที่ RLS เพียงอย่างเดียวไม่มีทางเปิดเผยได้อย่างปลอดภัย
flowchart LR
req["Incoming request"] --> edge["Edge Function"]
edge -->|"forwarded user token"| userClient["supabase-js client (anon key + user token)"]
edge -->|"service_role key"| adminClient["supabase-js client (service_role key)"]
userClient -->|"RLS applies"| db[("Postgres")]
adminClient -->|"RLS bypassed"| db