สถาปัตยกรรมของ Supabase
ไอเดียในหนึ่งประโยค
หัวข้อที่มีชื่อว่า “ไอเดียในหนึ่งประโยค”ทุก request ที่เข้ามาที่ auto-generated API ของ Supabase จะไปถึง Postgres พร้อมพก Postgres role ตัวใดตัวหนึ่งเสมอ — anon, authenticated หรือ service_role — และ role นี้แหละคือแกนหลักทางสถาปัตยกรรมที่ทุกอย่างอื่น (Data API, Row Level Security) สร้างต่อยอดขึ้นมา
ทุก request พก role มาด้วย
หัวข้อที่มีชื่อว่า “ทุก request พก role มาด้วย”auto-generated API ของ Supabase authenticate ทุก request หรือไม่ก็ปฏิบัติกับ request นั้นแบบ anonymous อย่างชัดเจน ไม่ว่าจะเป็นแบบไหน request จะไปถึง Postgres ในฐานะหนึ่งในสาม role นี้
anon— request ที่ไม่ได้ authenticate ไม่มี user login อยู่authenticated— request จาก user ที่ login แล้ว พก JWT claim มาด้วย รวมถึงauth.uid()สำหรับระบุว่าเป็น user คนไหนservice_role— server-side code ที่เชื่อถือได้ role นี้ bypass Row Level Security ทั้งหมด เป็น key ตัวเดียวกับที่คุณเจอในบทก่อนหน้า
-- Inside a Row Level Security policy, this is exactly how you tell roles apart.create policy "Users can view their own profile"on public.profilesfor selectto authenticatedusing (auth.uid() = id);พิสูจน์ด้วย RPC function จริง เรียกผ่าน Data API ได้ ที่แค่ report กลับมาว่า role ไหนเป็นคนรันมัน:
-- sql/fnd_whoami.sql (already applied — reference only, not re-collected by the harness)create or replace function public.fnd_whoami()returns table(who text, pg_role text)language sqlsecurity invokeras $$ select current_user::text, current_setting('role', true)::text;$$;
grant execute on function public.fnd_whoami() to anon, authenticated;$ curl -s -X POST "$API_URL/rest/v1/rpc/fnd_whoami" -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY" -H "Content-Type: application/json" -d '{}'[{"who":"anon","pg_role":"anon"}]
$ curl -s -X POST "$API_URL/rest/v1/rpc/fnd_whoami" -H "apikey: $SERVICE_ROLE_KEY" -H "Authorization: Bearer $SERVICE_ROLE_KEY" -H "Content-Type: application/json" -d '{}'[{"who":"service_role","pg_role":"service_role"}]function เดียวกัน connection pool เดียวกันข้างใน แต่ current_user เองเปลี่ยนไปตาม key ที่เซ็น request — ไม่ใช่ header ที่ PostgREST แค่อ่านแล้วเชื่อ แต่คือ Postgres session role จริงที่ query รันในฐานะนั้นจริง ๆ
PostgREST แปล HTTP เป็น SQL ไม่ enforce อะไรเอง
หัวข้อที่มีชื่อว่า “PostgREST แปล HTTP เป็น SQL ไม่ enforce อะไรเอง”PostgREST คือ service ที่แปล HTTP request แบบ GET /rest/v1/profiles?select=* ให้กลายเป็น SQL query จริง แล้วรัน query นั้น ในฐานะ role ที่ request นั้นพกมา จุดนี้สำคัญกว่าที่ดูเผิน ๆ PostgREST ไม่ได้ implement authorization system แยกของตัวเองเลย แต่พึ่งพา access control ของ Postgres ทั้งหมด — permission แบบ grant/revoke ธรรมดา และ policy Row Level Security — เพื่อตัดสินว่า role นั้นทำอะไรได้จริงบ้าง access control อยู่ที่ database ไม่ใช่ที่ API layer ที่อยู่ด้านหน้า
// This client call becomes an HTTP request to PostgREST, executed as// whatever role the current session carries (anon or authenticated).const { data, error } = await supabase .from('profiles') .select('id, username') .eq('id', userId);service อื่น ๆ ก็แค่อ่านเขียน Postgres เหมือนกัน
หัวข้อที่มีชื่อว่า “service อื่น ๆ ก็แค่อ่านเขียน Postgres เหมือนกัน”Auth, Storage และ Realtime เป็น service แยก (process แยก) แต่ไม่มีตัวไหนสร้าง storage layer ของตัวเองขึ้นมาใหม่เลย ทุกตัวสุดท้ายอ่านเขียน Postgres database เดียวกันนี้
- Auth (GoTrue) จัดการ schema
auth—auth.users, session และ table ที่เกี่ยวข้อง - Storage จัดการ metadata table ของตัวเอง (bucket, object, permission) ใน schema
storageตัวไฟล์จริงอยู่ใน object store แต่ access rule เป็น row ใน Postgres - Realtime อ่าน write-ahead log (WAL) ของ Postgres เพื่อ stream การเปลี่ยนแปลงของ row นอกเหนือจากการมี broadcast/presence channel แบบ ephemeral ให้ใช้
flowchart LR
client["Client request (anon / authenticated / service_role)"] --> postgrest["PostgREST"]
postgrest -->|"executes SQL as that role"| pg[("Postgres<br/>(grants + RLS enforced here)")]
auth["Auth (GoTrue)"] --> pg
storage["Storage"] --> pg
realtime["Realtime (reads WAL)"] --> pg อ่านโค้ดที่ AI สร้าง
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI สร้าง”AI assistant ถูกขอให้ “add a check that only admins can call this function” แล้วสร้างสิ่งนี้:
🐛 หา bug ในโค้ดนี้
-- An AI assistant's attempt at an "admin-only" RPC guardcreate or replace function public.fnd_admin_report()returns table(total bigint)language plpgsqlsecurity defineras $$beginif current_user != 'service_role' then raise exception 'admins only';end if;return query select count(*) from public.fnd_widgets;end;$$;เฉลย
สิ่งนี้ดูเหมือน guard จริง และมันจะทำงานได้จริงถ้าถูกเรียกด้วย service_role key ตรง ๆ กับ Postgres เท่านั้น — แต่มันถูกเขียนมาเพื่อ expose ผ่าน Data API เป็น RPC function ซึ่งทุก request ที่มาถึงมันผ่าน PostgREST มาในฐานะ role บางอย่าง อยู่แล้ว บั๊กคือ: security definer ทำให้ function นี้รันด้วยสิทธิ์ของ owner ของ function (ปกติคือ postgres) ซึ่ง proof จาก RPC request ข้างบนพิสูจน์ไปแล้วว่า current_user สะท้อน role ของ request ที่เรียก ไม่ใช่ owner — ดังนั้น check นี้เทียบผิดตัวไปเลยสำหรับ call ส่วนใหญ่ และแย่กว่านั้น อันตรายจริงของ function security definer (บท RPC ข้าง ๆ บทนี้พูดถึงลึกกว่านี้) คือมันต้องการ set search_path = '' เพื่อกัน schema-injection ซึ่งก็ขาดไปด้วยเช่นกัน ทางแก้ที่ตรงกับสถาปัตยกรรมของคอร์สนี้จริง ๆ: อย่าพยายามเช็ค current_user ข้างใน function security definer เลย — ไม่ว่าจะถอด security definer ออก (default เป็น security invoker แล้วให้ RLS กับ grant บน fnd_widgets ตัดสินสิทธิ์แบบปกติ) หรือถ้าจำเป็นต้องมีสิทธิ์ยกระดับจริง ๆ ให้เช็ค identity จริงของผู้เรียกผ่าน auth.uid() เทียบกับ flag profiles.is_admin (pattern ที่ module Edge Functions พิสูจน์ไว้) ไม่ใช่เทียบ current_user กับชื่อ role
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”มองทุก request ที่เข้า Data API ว่ามาถึง Postgres พร้อม name tag ติดตัวอยู่แล้ว — Postgres role — และทุกอย่างข้างล่าง (grant, RLS policy) อ่าน name tag นั้น ไม่ใช่ header หรือ body ของ request งานเดียวของ PostgREST คือเลือกว่าจะติด name tag ไหน (anon, authenticated หรือ service_role ตาม JWT ที่ได้รับมา) แล้วก็หลีกทางออกไป proof ข้างบนแสดงว่า name tag นี้ไม่ใช่เรื่องสมมติที่ PostgREST แค่จำไว้ในใจ แต่คือ current_user จริงที่ Postgres เองเห็นตลอด request นั้น
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”- การบังคับ access control ไว้ใน Postgres ทั้งหมด (grant + RLS) ทำให้ client ทุกตัว — REST, GraphQL, client ในอนาคตที่คอร์สนี้ยังไม่ได้พูดถึง — ได้ guarantee เดียวกันอัตโนมัติ — แลกมาด้วยต้องคิดแบบ role กับ policy แทนที่จะเป็น check แบบ “if (user.isAdmin)” ที่คุ้นเคยกว่า พิสูจน์ข้างบนแล้วว่าพังพอดีแบบนั้นเมื่อพอร์ตแบบไม่ระวังเข้าไปใน function security definer
security definerให้ function ทำอะไรบางอย่างที่ role ของผู้เรียกเองทำไม่ได้โดยตรง (pattern admin-report แบบที่ทำถูก) — แลกมาด้วย function เองกลายเป็น access-control boundary ทั้งหมด เพราะ Postgres ไม่ apply ข้อจำกัดของผู้เรียกเองข้างในมันเลย- PostgREST แปล HTTP เป็น SQL ตรง ๆ ทำให้ API surface sync กับ schema อัตโนมัติ — แลกมาด้วย grant และ RLS ของ schema เองเป็นสิ่งเดียวที่กั้นระหว่าง “reachable” กับ “secure” ไม่มี application-layer authorization code สำรองไว้เผื่อ rule ระดับ database ผิด
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “มีแค่ anon กับ authenticated ที่สำคัญจริง — service_role แค่ convenience สำหรับ script” proof whoami ข้างบนปฏิบัติกับมันเหมือนกันทุกอย่าง: มันคือ Postgres role จริงเหมือนอีกสองตัว แค่มี attribute
bypassrlsเพิ่มมา ไม่มีอะไร special-case นอกจาก attribute นั้นตัวเดียว - “function security definer รู้อัตโนมัติว่าผู้เรียกตัวจริงคือใคร” ถูกหักล้างด้วย SpotTheBug ข้างบน: current_user ข้างใน function security definer สะท้อน owner ของ function ไม่ใช่ผู้เรียก ซึ่งเป็นเหตุผลที่เช็ค current_user เพื่อ authorization ข้างในมันกลับตาลปัตรเลย
- “PostgREST มีระบบ user/session ของตัวเองแยกจาก Postgres” ไม่มี session store แยกเลย — พิสูจน์ข้างบนแล้วว่ากลไก authorization ทั้งหมดของ PostgREST คือการเลือกว่าจะรัน query ในฐานะ Postgres role ตัวไหน