ข้ามไปยังเนื้อหา

Edge Functions กับฐานข้อมูล

ใน Edge Function คุณ query ฐานข้อมูลด้วย supabase-js ได้เหมือนโค้ดฝั่ง client เป๊ะ ๆ แต่ key ที่ใช้สร้าง client นั้นต่างหากที่ตัดสินว่า Row Level Security จะทำงานหรือถูก bypass ไปเลย

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 มีอยู่แล้ว

ทางเลือกอีกแบบคือสร้าง 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

สมมติว่าคุณต้องการ 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
สองวิธีสร้าง Supabase client ใน Edge Function
เกิดอะไรขึ้นกับ RLS เมื่อ Edge Function forward auth token ของผู้เรียกไปให้ supabase-js
เกิดอะไรขึ้นกับ RLS เมื่อ Edge Function ใช้ service_role key
การใช้ service_role key ใน function เป็นตัวเลือกที่เหมาะสมเมื่อไหร่
ทำไมการใช้ service_role ถึงผลักภาระไปที่โค้ดของ function