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

อภิธานศัพท์

ศัพท์ของ Supabase วางซ้อนกันหลายชั้นบน Postgres ธรรมดา: ชั้น role กับ grant (anon/authenticated/service_role, [api] schemas) ที่ตัดสินว่า request จะไปถึง table ได้หรือไม่; Row Level Security (using/with check, permissive/restrictive) ที่ตัดสินว่าเห็น row ไหนได้บ้างเมื่อไปถึงแล้ว; Auth (GoTrue, JWT, Auth Hook) ที่ตัดสินว่า request นั้นอ้างว่าเป็นใคร; Data API ที่ auto-generate ขึ้นมา (PostgREST, pg_graphql, RPC) ที่แปลง schema ให้กลายเป็น endpoint; Edge Functions (Deno, โมเดล isolate) สำหรับ logic ที่ไม่ควรอยู่ใน SQL; Realtime กับ Storage (WAL replication, realtime.messages, storage.objects) ที่สร้างขึ้นจาก row และ policy ของ Postgres ธรรมดาข้างใต้ทั้งหมด; และเครื่องมือฝั่ง client กับ production (supabase-js, Supavisor, migration, branching) ที่ผูกทุกอย่างเข้าด้วยกัน เวลาอ่านบทเรียนแล้วเจอคำที่ไม่คุ้น ให้กลับมาดูที่นี่ — ทุกคำนิยามด้านล่างมาจากบทเรียนที่พิสูจน์กับ Supabase stack จริงที่รันในเครื่องของคอร์สนี้ (Postgres 17.6, PostgREST, GoTrue, Realtime, Storage, Edge Runtime) ไม่ใช่จากความจำ

Termความหมายบทเรียน
aal / amrclaim สอง claim ใน JWT ที่บอกว่า session นี้ authenticate มาอย่างไร: aal (Authenticator Assurance Level) เป็น aal1 สำหรับปัจจัยเดียว และ aal2 เมื่อผ่าน MFA แล้ว; amr (Authentication Methods Reference) เป็น array บันทึกแต่ละวิธีที่ authenticate พร้อม timestamp ([{"method":"password","timestamp":...}]) และจะมี entry ที่สองเพิ่มขึ้นถ้า session ถูกอัปเกรดด้วย MFA ภายหลังSessions and JWTs
admin API (supabase.auth.admin.*)endpoint /auth/v1/admin/* ที่ใช้ได้เฉพาะ service_role — พิสูจน์แล้วด้วย 403 not_admin จริงเมื่อเรียกด้วย anon key ต่างจาก signUp() ตรงที่ admin.createUser() ต้องใช้ service_role และมีไว้ให้ server ที่เชื่อถือได้สร้าง account แทน คนอื่น ไม่ใช่สิ่งที่ควรเปิดให้ browser เรียกได้Authentication Methods
anon key / anon rolepublic API key ที่ปลอดภัยพอจะใส่ไว้ใน client code ระบุว่า request ที่ยังไม่ authenticate ทุกตัวใน Data API เป็น Postgres role anon — ถูก Row Level Security บังคับใช้เหมือน role อื่น ๆ ทุกประการ RLS เองไม่ได้ปฏิบัติต่อมันเป็นพิเศษ ตั้งแต่ปลายปี 2025 โปรเจกต์ใหม่จะออก key นี้เป็น string สั้นลงรูปแบบ sb_publishable_... ด้วย ซึ่งทำหน้าที่เดียวกันทุกประการProject Setup and the Supabase CLI
anonymous sign-insession จริงที่เป็น role authenticated มี row ของตัวเองใน auth.users และ is_anonymous: true — ไม่เหมือนกับ request ที่ยังไม่ authenticate แบบ role anon เพราะ auth.uid() คืนค่าจริงให้ ช่วยให้ product เลื่อนกำแพง signup ออกไปได้ แลกกับการมี row ใน auth.users ของคนที่อาจไม่เคย convert เลยจนกว่าจะผ่านขั้นตอน linkIdentity แบบชัดเจนAuthentication Methods
asymmetric signing (ES256 vs HS256)ค่า default จริงตั้งแต่ 1 ตุลาคม 2025 สำหรับทุกโปรเจกต์ใหม่ (อ้างจาก supabase.com/blog/jwt-signing-keys): access token จริง decode ออกมาเป็น {"alg":"ES256","kid":"..."} ตรวจสอบได้ด้วย public key ผ่าน JWKS โดยไม่ต้องมี shared secret เลย ส่วน anon/service_role API key แบบ static เป็นกลไกเก่าแยกต่างหาก — {"alg":"HS256"} ไม่มี kid เซ็นด้วย JWT_SECRET แบบ shared แบบเก่า และไม่เคยถูกย้ายมาใช้ scheme แบบ per-session ใหม่นี้เลยSessions and JWTs
auth.identitiestable แยกจาก auth.users หนึ่ง row ต่อคู่ (provider, person) หนึ่งคู่ — คน ๆ เดียวกัน authenticate ได้หลายวิธีในเวลาต่างกัน (email แล้วค่อย GitHub) แต่ละหลักฐานเป็น row ของตัวเองที่ชี้กลับไปที่ row เดียวใน auth.usersAuthentication Methods
auth.uid() / auth.jwt() / auth.role()สามฟังก์ชันที่ policy ของ RLS ทุกอันสร้างมาจาก อ่านจาก request.jwt.claims ที่ PostgREST ส่งเข้ามาใน session ของ Postgres: auth.uid() คืนค่า claim sub (เป็น null สำหรับ anon/service_role เพราะทั้งคู่ไม่ใช่ user ที่ signed-in จริง), auth.role() มีค่าเสมอ (ทุก request มี claim role), และ auth.jwt() คืน object claim ที่ decode มาทั้งก้อน — จึงเป็นเหตุผลที่ custom claim อย่าง tenant_id ต้องอ่านผ่าน (auth.jwt() ->> 'tenant_id') แทนที่จะมีฟังก์ชันเฉพาะRow Level Security Fundamentals
auth.userstable ที่ Auth service (GoTrue) ของ Supabase จัดการให้ — pattern มาตรฐานสำหรับผูกข้อมูลแอปเข้ากับ user คือ table profiles ที่ id เป็น foreign key ไปยัง auth.users(id)Tables, Schema, and Migrations
Auth Hook (Customize Access Token Claims)ฟังก์ชัน Postgres ที่ GoTrue เรียกตอนออก token กำหนดผ่าน [auth.hook.custom_access_token] ใน config.toml รับ event เป็น jsonb (user_id + claim ที่ประกอบไว้แล้ว) และคืน {"claims": {...}} — เป็นกลไกเดียวที่ใส่ custom claim อย่าง tenant_id เข้าไปใน JWT ได้ เพราะ GoTrue เองไม่เคยใส่ให้เอง เรียกได้เฉพาะ supabase_auth_admin เท่านั้น และ hook มีผลกับ token ที่ออก ใหม่ เท่านั้น (sign-in หรือ refresh ครั้งถัดไป) ไม่มีผลย้อนหลังกับ token ที่ออกไปแล้วRLS Patterns and Pitfalls
authenticated rolePostgres role ที่ request ซึ่ง log in แล้วทุกตัวถืออยู่ พร้อม JWT claim (รวม auth.uid()) ที่ policy ของ RLS ใช้ได้ — ต่างจาก anon ตรงที่ PostgREST execute query ด้วย role ไหน ตาม JWT ของ request นั้นThe Supabase Architecture
authenticator rolePostgres role แบบ login ตัวเดียวที่ PostgREST เปิด database connection ด้วยจริง ๆ — พิสูจน์ผ่าน \du: anon/authenticated/service_role ทุกตัวบอกว่า Cannot login มีแค่ authenticator เท่านั้นที่ login ได้ PostgREST ทำ SET LOCAL ROLE จริงไปเป็นหนึ่งในสามตัวนั้นตามที่ JWT ของ request อ้าง ต่อ transaction — session_user ยังเป็น authenticator ตลอด มีแค่ current_user ที่เปลี่ยนPostgres Roles and Grants
auto_expose_new_tablesค่าตั้งค่าใน config.toml ที่ควบคุมว่า table ใหม่เอี่ยมใน schema public จะถูก grant ให้ anon/authenticated/service_role อัตโนมัติโดยไม่ต้อง grant เองหรือไม่ พิสูจน์แล้วจริง: ค่า default ของ CLI ที่ scaffold ในเครื่องตกกลับไปเป็น true ทั้งที่ฝั่ง hosted กำลัง rollout ค่า false เป็น default ใหม่แบบเป็นขั้นตอน — เปิดให้เลือกเองได้ตั้งแต่ 28 เมษายน 2026, เป็น default สำหรับโปรเจกต์ใหม่ตั้งแต่ 30 พฤษภาคม 2026, และบังคับใช้กับโปรเจกต์เดิมทั้งหมดตั้งแต่ 30 ตุลาคม 2026 (changelog)Securing the Data API
Termความหมายบทเรียน
branch (Supabase branch)Postgres instance แยกต่างหากจริง ๆ — มี schema, Data API, Auth, และ Storage ของตัวเอง — สร้างขึ้นเพื่อมีอายุเท่ากับ Git branch หรือ pull request ไม่ใช่แค่ feature flag ใน database ที่ใช้ร่วมกัน supabase branches create --persistent จะอยู่ต่อไปแม้ push ใหม่เรื่อย ๆ ส่วน --with-data จะ clone ข้อมูล production เข้ามาด้วยBranching and the Migrations Workflow
broadcastชั้น messaging ชั่วคราวของ Realtime ที่ไม่ขึ้นกับ database เลย: channel.on('broadcast', ...) รับ payload JSON อะไรก็ได้ที่ส่งมาด้วย channel.send() โดยไม่ต้องต่อสาย table แบบ postgres_changes เลยRealtime Broadcast and Presence
broadcast_changes() (realtime.broadcast_changes())ฟังก์ชัน Postgres ที่เรียกจาก trigger เพื่อส่ง payload รูปทรงเดียวกับ postgres_changes (payload.payload.record) ผ่าน Broadcast แทนเส้นทาง RLS แบบ per-subscriber — พิสูจน์แล้วว่าเขียนด้วย private = true เสมอ จึงต้องตั้ง RLS บน realtime.messages เหมือน private channel อื่นทุกประการ ไม่มีโหมด broadcast แบบ public ให้เลยRealtime Broadcast and Presence
bucketcontainer ระดับบนสุดของ Storage เป็นแค่ row ใน storage.buckets (createBucket เป็น wrapper บาง ๆ ของ insert เดียวกันนั้นเอง) ตั้งเป็น public ได้ (อ่านผ่าน URL ได้โดยไม่ต้อง auth) หรือ private (คุมการเข้าถึงต่อ-request ผ่าน storage.objects RLS)Storage Buckets and Policies
BYPASSRLSattribute ของ Postgres role ที่ service_role มี พิสูจน์แล้วว่าข้าม RLS ไปเลยทั้งหมด ไม่ขึ้นกับ policy ใด ๆ — Postgres ไม่แม้แต่จะไปหา policy ที่ตรงกันให้ role ที่มี BYPASSRLS เลย ไม่ว่า Data API จะ expose schema นั้นหรือไม่Securing the Data API
Termความหมายบทเรียน
cache-controlHTTP response header ธรรมดา ตั้งค่าต่อ-object ตอน upload ผ่าน option cacheControl และถูก echo กลับแบบ คำต่อคำ — พิสูจน์แล้วว่า cacheControl: '3600' (ตัวอย่างจาก docs เอง) ทำให้ได้ header ตัวจริงที่ไม่ตรงมาตรฐาน cache-control: 3600 ไม่ใช่ max-age=3600 โดยไม่มีการตรวจสอบความถูกต้องเลย ทำงานเหมือนกันไม่ว่าจะเปิด image transformation หรือไม่Image Transformations and CDN
channelการเชื่อมต่อ Realtime ที่ตั้งชื่อไว้ (supabase.channel('name')) ที่รับ subscription ทั้ง postgres_changes, broadcast, และ presence พร้อมกันได้ — การ authorize ของ private channel และ filter ของ postgres_changes ทั้งคู่อิง string channel/topic เป๊ะ ๆ นี่คือเหตุผลที่คอร์สนี้ตั้งชื่อ channel แบบ scope เจาะจงเสมอ (rt:private-room-1)Realtime Broadcast and Presence
CI (migrations through CI)การรัน supabase db push เป็นส่วนหนึ่งของ pipeline เดียวกับที่ทดสอบ migration บน preview branch มาแล้ว แทนที่จะให้ใครสักคนวาง SQL ลง production ด้วยมือ — ขั้นตอนที่ทำให้ migration ถูก apply “แบบเดียวกันทุกครั้ง” แทนที่จะ drift ออกจาก version controlBranching and the Migrations Workflow
config.tomlไฟล์ตั้งค่า CLI ของโปรเจกต์ในเครื่อง — [api] schemas, auto_expose_new_tables, [auth.hook.custom_access_token], [functions.<name>] verify_jwt, refresh_token_reuse_interval และอื่น ๆ — ทุก gate ที่คอร์สนี้พิสูจน์ว่าเป็น error หรือพฤติกรรมจริง ล้วนสืบกลับไปที่ key จริงในไฟล์เดียวนี้Securing the Data API
CORS preflightrequest OPTIONS ที่ browser ส่งเองก่อน request ข้าม origin ที่ไม่ธรรมดา ถูกตรวจสอบฝั่ง browser ล้วน ๆ — curl ไม่บังคับ CORS เลย ดังนั้น 200 จาก curl พิสูจน์แค่ว่า function รันได้ แต่ไม่พิสูจน์อะไรเลยว่า browser client ข้าม origin จริงจะอ่าน response ได้หรือไม่ บน local stack นี้ Kong gateway ของ CLI เองตอบ OPTIONS ก่อนที่ branch req.method === 'OPTIONS' ของ function เองจะถูกเรียกถึงด้วยซ้ำ — เป็นความแปลกเฉพาะในเครื่อง ส่วน Edge Functions บน hosted คาดหวังให้ function เองเป็นคนตอบWriting and Invoking a Function
count=exact / count=planned / count=estimatedสามโหมดของ Prefer: count=... รายงานผ่าน Content-Range: exact รัน count(*) จริง; planned อ่านค่าประมาณของ planner (พิสูจน์แล้วได้ 720 บน table 3 row จริงที่ยังไม่ ANALYZE — ไม่ใช่ bug แต่เป็นสิ่งที่ planner เดาจริง ๆ); estimated ใช้ค่านับจริงถ้าต่ำกว่า db-max-rows แล้วถอยไปใช้ planned ถ้าเกินThe REST API (PostgREST)
createSignedUrl / signed URLURL แบบจำกัดเวลาสำหรับ object ใน private bucket ฝัง JWT ที่เซ็นจริงพร้อม claim exp ที่ตรวจฝั่ง server — พิสูจน์แล้วว่า request หลังหมดอายุจะได้ error InvalidJWT จริง ไม่ใช่แค่ hint ฝั่ง client ที่ยังใช้ได้เงียบ ๆ ต่างจาก getPublicUrl ที่สร้าง URL โดยไม่ตรวจสอบอะไรเลย แม้กับ object ใน private bucketStorage Buckets and Policies
Termความหมายบทเรียน
Data API exposureด่านแรกจากสองด่านที่เป็นอิสระต่อกันที่ request ต้องผ่าน: Postgres grant (ให้ anon/authenticated/service_role) ตัดสินว่า table หรือ function เข้าถึงผ่าน PostgREST ได้เลยหรือไม่ ประเมินก่อนที่ RLS จะทำงานเสียอีก table ที่มี RLS แบบ permissive แต่ไม่มี grant จะได้ 42501 จริง ไม่ใช่ผลลัพธ์ที่ถูก RLS กรอง — RLS ไม่เคยถูกเรียกดูเลยSecuring the Data API
Database WebhookPostgres trigger ที่สร้างบน pg_net เรียก supabase_functions.http_request(url, method, headers, body, timeout) เมื่อมี insert/update/delete — พิสูจน์แล้วว่า asynchronous เต็มรูปแบบ (insert จริงคืนค่าใน 2.767ms น้อยกว่า HTTP round trip ที่มันไป trigger มาก) ส่ง { type, table, record, schema } และ ไม่มี retry ในตัว ต่างจาก Auth Hook — ฝั่งรับต้อง idempotent เองBackground Tasks and Webhooks with Functions
db diff (supabase db diff --schema public)จับการเปลี่ยนแปลง schema ที่ทำผ่าน Studio GUI มาเป็นไฟล์ migration .sql จริงภายหลัง ทำให้การแก้แบบ ClickOps ไม่ล่องหนจากการ review code และ version controlTables, Schema, and Migrations
db lint (supabase db lint)type-checker ของ plpgsql สำหรับ body ของฟังก์ชัน — ไม่ใช่ advisor ของ RLS หรือความปลอดภัย แม้ชื่อจะชวนให้คิดแบบนั้น พิสูจน์สดแล้ว: รันกับ schema ที่มีช่องว่าง RLS จริงอยู่ (table ที่เปิด RLS แล้วไม่มี policy เลย) มันรายงาน 0 ผล; รันกับฟังก์ชันที่มี type mismatch ข้างในจริง มันจับ 42804 จริงได้ก่อนที่ฟังก์ชันจะถูกเรียกด้วยซ้ำVerification Tools
db push (supabase db push)apply ทุก migration ที่ยังไม่เคยรันเข้ากับโปรเจกต์หรือ branch ที่ชี้ไปอยู่ — คำสั่งเดียวกันไม่ว่าจะเป็น preview branch หรือ production ซึ่งเป็นเหตุผลที่ทำให้ปลอดภัยพอจะรันจาก CITables, Schema, and Migrations
db reset (supabase db reset)replay ไฟล์ migration ทุกไฟล์ใน supabase/migrations/ จากศูนย์กับ database ใหม่ แล้วรัน supabase/seed.sql — กลไกที่ทำให้ schema change ที่พิมพ์ตรงเข้า psql/Studio และไม่เคยถูกจับเป็นไฟล์ migration หายไปทั้งหมดตอน reset ครั้งถัดไป พิสูจน์ด้วยตัวอย่าง “ClickOps with extra steps” จริงTables, Schema, and Migrations
Denoruntime ที่ Edge Functions รันจริง — ไม่ใช่ Node.js มี Deno.serve เป็น global, รองรับ TypeScript แบบ first-class ไม่ต้องมีขั้นตอน build และ sandbox ปลอดภัยเป็น default — code ที่คาดหวัง API ของ Node.js หรือ import แบบ npm เปลือย ๆ จะใช้ไม่ได้ตรง ๆWhat Are Edge Functions
deno checkขั้นตรวจ type ก่อน deploy บน module graph ของ function เอง — จับ error การ resolve import แบบเดียวกับที่จะเจอเป็น 503 ตอน boot ได้ตั้งแต่ก่อนเรียก supabase functions serve หรือ deployWriting and Invoking a Function
deno.json (import map)import map เฉพาะ function วางไว้ข้าง index.ts ของ function นั้น scope แค่ function นั้นเดียวเพราะแต่ละ function มี module graph ของตัวเอง — ใช้แทน bare specifier อย่าง 'supabase' ให้กลายเป็น jsr: specifier จริง function ข้าง ๆ ต้องมีสำเนาของตัวเอง ไม่มี config รากที่ share กันเลยWriting and Invoking a Function
Termความหมายบทเรียน
Edge FunctionTypeScript ฝั่ง server ที่รันบน Deno ข้างใน Edge Runtime สำหรับ logic ที่ไม่เหมาะอยู่ใกล้ Postgres — เรียก API บุคคลที่สาม, ถือ secret, งานหนัก CPU, หรือ HTTP endpoint แบบกำหนดเองเต็มที่ — เรียกผ่าน supabase.functions.invoke() ไม่ใช่เครื่องมือที่เหมาะกับ “aggregate/filter/join แถวข้อมูล” ซึ่ง Postgres RPC function ทำได้เร็วกว่าโดยไม่ต้องมี network round tripWhat Are Edge Functions
Edge Runtime (main runtime / user runtime, isolate)engine โอเพนซอร์ส (github.com/supabase/edge-runtime) ที่ทั้ง supabase functions serve และ hosted platform ใช้รัน แบ่งเป็น main runtime (proxy request ไม่มีข้อจำกัด อ่าน env var ได้ทุกตัว) กับ user runtime — Deno isolate แบบ sandbox ต่อ function หนึ่งตัว มี limit memory/CPU จริง พิสูจน์แล้วสด ๆ: hostname ที่ resolve ได้จาก docker exec เข้า container กลับ ENOTFOUND เมื่อเรียกจาก ข้างใน isolate ของ function — ขอบเขต sandbox ที่สังเกตเห็นได้จริง ไม่ใช่แค่ทฤษฎี isolate แบบ cold วัดได้ช้ากว่าแบบ warm ประมาณ 10 เท่าบน Mac เครื่องนี้What Are Edge Functions
EdgeRuntime.waitUntilทำเครื่องหมาย promise ว่าเป็นงานเบื้องหลังที่ยังทำงานต่อแม้ response ของ function จะถูกส่งไปแล้ว — พิสูจน์ด้วย timestamp จริง: response กลับมาใน 60ms ในขณะที่งานเบื้องหลังที่ส่งให้ waitUntil ยังทำงานอยู่อีก 3 วินาทีข้างใต้ โดยผู้เรียกไม่รู้เลย การ await มันตรง ๆ แทนจะทำลายกลไกทั้งหมดBackground Tasks and Webhooks with Functions
embedding / !innerPostgREST เดินตาม foreign-key constraint จริงเพื่อฝัง row ที่เกี่ยวข้องมาในคำขอเดียว (?select=title,director:directors(name)) — พฤติกรรม default เป็น left join (row ที่ไม่ match ยังโผล่มา โดย field ที่ฝังเป็น null); เติม !inner บังคับให้เป็น inner join แทน ตัด row ที่ไม่ match ออกไปเลย และยังใช้ filter บน column ของ table ที่ฝังได้ด้วยThe REST API (PostgREST)
error.code (PostgrestError)field ที่เสถียร (SQLSTATE ของ Postgres อย่าง 23505 หรือ code เฉพาะของ PostgREST อย่าง PGRST205) ที่ควรใช้ตัดสินใจแบบโปรแกรม — ไม่ใช่ error.message ที่เป็นข้อความให้คนอ่านและเปลี่ยนคำได้ระหว่าง version PostgrestError ที่มีค่ายังมี message, details, และ hint ด้วยError Handling and Typed Queries
exposed schemas ([api] schemas)รายชื่อ schema ใน config.toml (default ["public", "graphql_public"]) ที่ PostgREST จะ introspect เลย ไม่ขึ้นกับ grant ระดับ table table ที่ grant ครบถ้วนแต่อยู่ใน schema ที่ไม่อยู่ในรายชื่อนี้ ยังได้ 404 PGRST205 "Could not find the table" จริง — แยกไม่ออกจาก table ที่ไม่มีอยู่จริงเลย ซึ่งเป็นกับดักพอดีในตัวอย่างคอมเมนต์ AI-generated ที่เขียนว่า “expose ผ่าน service_role” โดยไม่เคยเช็คชั้นนี้Securing the Data API
explain analyzeวิธีตรงที่สุดในการดูว่า Postgres ใช้ index scan หรือ sequential scan สำหรับ query หนึ่ง ๆ พร้อมเวลา execution จริง — พิสูจน์บน table 100,000 row จริง ลดจาก 86.8ms (Seq Scan, auth.uid() ถูกประเมินใหม่ทุก row) เหลือ 3.5ms (ห่อด้วย select cache เป็น InitPlan) จนถึง 0.096ms (เพิ่ม index เข้าไปด้วย) ควรรันกับทุก query ที่มี RLS คุ้มครองก่อน ship ไม่ใช่หลังจากช้าไปแล้วใน productionRLS Patterns and Pitfalls
Termความหมายบทเรียน
functions.invoke()เมธอดของ supabase-js สำหรับเรียก Edge Function แทนที่จะใช้ fetch ธรรมดา — serialize body เป็น JSON และแนบ header Authorization ให้อัตโนมัติ (access token ของ user ที่ signed-in ถ้ามี ไม่งั้นใช้ anon key ของโปรเจกต์)Writing and Invoking a Function
Termความหมายบทเรียน
gen types (supabase gen types typescript --project-id <ref>)ตรวจ schema ที่ใช้งานจริงแล้วสร้าง type TypeScript Database มีรูปทรง Row/Insert/Update ต่อ table — เมื่อส่งเป็น generic ให้ createClient<Database>() จะเปลี่ยน column ที่สะกดผิดหรือ field ที่จำเป็นแต่ขาดตอน insert ให้กลายเป็น compile-time error แทนที่จะเซอร์ไพรส์ตอน runtimeThe supabase-js Client
getSession() vs getUser() vs getClaims()getSession() อ่านจาก local storage โดย ไม่ตรวจ signature เลย — พิสูจน์แล้วว่าเชื่อ claim role/sub ที่ถูกปลอมของ token ที่ถูกแก้ไขทั้งหมด getUser() ส่ง token ไปยัง Auth server จริง (GET /auth/v1/user) ซึ่งปฏิเสธ token ที่ถูกแก้ไขตัวเดียวกันนั้นด้วย error bad_jwt จากการตรวจ crypto จริง getClaims() ได้การรับประกันทาง cryptographic แบบเดียวกัน ในเครื่อง ตรวจกับ public key ของ JWKS ที่ cache ไว้ แทนที่จะต้อง round trip ทาง network — ตัวอย่าง Next.js middleware อย่างเป็นทางการของ Supabase ปัจจุบันเรียก getClaims() ไม่ใช่ getUser() ด้วยเหตุผลนี้Sessions and JWTs
GoTrueserver โอเพนซอร์สที่ Supabase Auth สร้างอยู่บน — ทุกวิธี sign-in (password, magic link, OTP, anonymous, OAuth) เป็นการเรียก HTTP ธรรมดาไปยัง GoTrue server จริง ที่จบลงแบบเดียวกันเสมอ: มี row ใน auth.users ที่ match หรือถูกสร้างขึ้นAuthentication Methods
grant / revokeคำสั่ง Postgres ธรรมดาที่อยู่ข้างใต้ทุกรูปแบบของ Data API exposure ไม่ว่าจะ auto-grant, dashboard, หรือเขียน SQL เอง การ revoke สิทธิ์ select ของ anon บน table หนึ่ง ทำให้ได้ 42501 จริง พร้อม hint ที่บอกคำสั่ง grant ที่แก้ปัญหาได้ตรง ๆ — พิสูจน์แล้วว่า RLS ไม่เคยทำงานเลยเพราะ request ไปไม่ถึงจุดนั้นSecuring the Data API
Termความหมายบทเรียน
imgproxyservice แยกต่างหากที่ Storage ส่งงาน image transformation ไปให้ ถูกล็อกไว้หลัง flag ENABLE_IMAGE_TRANSFORMATION (ปิดเป็น default บน local stack นี้และบน hosted plan ที่ต่ำกว่า Pro) — URL transform ถูกสร้างขึ้นโดยไม่ตรวจสอบเลยว่าเปิด transformation หรือยัง จะพังเป็น 404 จริงก็ต่อเมื่อ client ขอมาจริง ๆ เท่านั้นImage Transformations and CDN
import specifier (jsr: / npm:)Deno ไม่มีการ resolve bare specifier แบบ node_modules เลย — import { createClient } from '@supabase/supabase-js' แบบเปลือย ๆ จะพังตั้งแต่ boot ด้วย 503 BOOT_ERROR จริง ก่อนที่ handler code จะรันด้วยซ้ำ jsr:@supabase/supabase-js@2 และ npm:nanoid@5 ใช้ได้ทั้งคู่ เมื่อ pin version ไว้ ถ้าไม่ pin จะ resolve เป็น “อะไรก็ตามที่ล่าสุด ณ ตอนนั้น” โดยไม่มี lockfileWriting and Invoking a Function
index scan / sequential scanสอง query plan ที่บรรทัดบนสุดของ explain analyze บอกชื่อไว้: Seq Scan อ่านทุก row แล้วเช็คแต่ละแถวกับ filter (รวม policy condition ของ RLS ด้วย); Index Scan/Bitmap Heap Scan กระโดดตรงไปยัง row ที่ match ผ่าน index คอลัมน์ที่ RLS policy อ้างถึงแต่ไม่มี index จะบังคับให้เกิดแบบแรกเงียบ ๆ ในทุก query กับ table นั้น ไม่ว่า table จะใหญ่แค่ไหนPerformance and Indexing
Termความหมายบทเรียน
JWKSendpoint public key (/auth/v1/.well-known/jwks.json) ที่ service ใช้ตรวจ signature ของ token แบบ asymmetric (ES256) โดยไม่ต้องมี shared secret เลยที่ไหน — response จริงในเครื่องมี kid ตรงกับ kid ใน header ของ session token จริงทุกตัวเป๊ะ ๆSessions and JWTs
JWT claims (sub/role/aud/exp/iat/session_id)claim มาตรฐานที่ access token จริงที่ decode แล้วมี: sub คือสิ่งที่ auth.uid() อ่าน; role คือสิ่งที่ auth.role() อ่านและเป็นสิ่งที่ Postgres set role จริง ๆ; aud ต้องเป็น authenticated PostgREST ถึงจะรับ token; exp/iat คือ Unix timestamp วันหมดอายุ/วันออก; session_id ผูก token กับ session sign-in หนึ่งครั้ง ใช้โดย refresh-token rotationSessions and JWTs
Termความหมายบทเรียน
KongAPI gateway ในเครื่องของ CLI ที่อยู่หน้า Edge Functions — พิสูจน์แล้วว่าดักและตอบ OPTIONS preflight request ไปยัง /functions/v1/* ด้วย CORS header ของตัวเองก่อนที่ code ของ function เองจะถูกเรียกถึงด้วยซ้ำ เป็นพฤติกรรมเฉพาะในเครื่องที่ไม่สะท้อน hosted platformWriting and Invoking a Function
Termความหมายบทเรียน
magic link / OTPสองรูปแบบ sign-in ที่เกี่ยวข้องกันแต่กลไกต่างกัน: magic link คือ URL แบบ GET ที่คลิกได้ มี token ฝังอยู่ในตัว; phone OTP คือรหัสสั้น ๆ ที่พิมพ์กลับมาแล้วแลกผ่าน verifyOtp (SMS ไม่มี browser context ให้คลิก) response ของ /auth/v1/otp ตั้งใจให้ว่างเปล่า ({}) — token ที่ใช้ครั้งเดียวจริงจะโผล่มาเฉพาะในอีเมลจริงเท่านั้น พิสูจน์แล้วด้วยการดึงมาจาก MailpitAuthentication Methods
Mailpitกล่องรับอีเมลในเครื่องที่บทพิสูจน์ของคอร์สนี้ดึงอีเมล magic-link จริงมาผ่าน API ของมันเอง — พิสูจน์ว่า token ถูกส่งผ่านอีเมลเท่านั้นจริง ๆ ไม่เคยรั่วออกมาใน response ของ API เองAuthentication Methods
MCP execute_sqltool SQL ของ Supabase MCP server เข้าถึงได้ที่ MCP_URL จริงที่ supabase start print ออกมา — พิสูจน์แล้วว่ารันในฐานะ postgres database superuser ตัวจริง (current_user/session_user รายงานว่า postgres ทั้งคู่) ซึ่งข้าม Row Level Security เหมือนกับ service_role key ที่รั่วไหลทุกประการ: table ที่มี row เดียวถูก scope ด้วย RLS ให้ user เฉพาะคนหนึ่งคืน row นั้นผ่าน execute_sql โดยไม่มี session หรือ key ของตัวเองเลยนอกจาก MCP connectionVerification Tools
migration (supabase/migrations/)ไฟล์ .sql แบบมี timestamp ธรรมดา สร้างด้วย supabase migration new <name> (ว่างเปล่า เขียนเอง) หรือ supabase db diff (จับมาจากการเปลี่ยนแปลงผ่าน GUI) — กลไกที่เปลี่ยน schema change ให้กลายเป็นสิ่งที่ review ได้, ทำซ้ำได้, และ deploy ไปที่อื่นได้ผ่าน supabase db push แทนที่จะเป็น ClickOps ที่มองไม่เห็นTables, Schema, and Migrations
Termความหมายบทเรียน
OAuth providerส่ง user ไปยังบุคคลที่สาม (Google, GitHub, …) ผ่าน signInWithOAuth() โดยกลไกแล้วลงเอยที่ table เดียวกันกับวิธีอื่นทุกวิธี — row ใหม่ใน auth.identities ด้วย provider = '<name>' ผูกกับ auth.users row ใหม่หรือของเดิมถ้าอีเมลที่ได้กลับมาตรงกับที่มีอยู่แล้ว เรียกไม่ได้จาก local stack ที่ไม่มี credential นี้ (block [auth.external.<provider>] เป็น enabled = false เป็น default)Authentication Methods
onAuthStateChangesubscription เดียว มักผูกไว้ตอนแอป start ที่ fire ทุก event เกี่ยวกับ auth (SIGNED_IN, SIGNED_OUT, TOKEN_REFRESHED, และ session แรกตอนโหลด) — ทำให้ UI state ซิงก์กับ session จริงของ client โดยไม่ต้องเช็คเองใหม่หลังทุก auth actionAuth in the Client
Termความหมายบทเรียน
permissive / restrictive policypolicy แบบ permissive (default) ของ operation เดียวกันรวมกันด้วย OR — match อันไหนอันหนึ่งก็อนุญาต row นั้น ส่วน policy แบบ restrictive (as restrictive) รวมกันด้วย AND บนทุก permissive ที่ match แล้ว — พิสูจน์แล้วสด ๆ: policy แบบ restrictive ที่ block user ที่ถูกแบนได้ block แม้แต่ row ที่ permissive public-read policy จะโชว์ให้เห็นอยู่แล้ว เหมาะกับกฎแบบ cross-cutting ที่บังคับใช้เสมอโดยไม่ต้องเขียนซ้ำในทุก permissive policyRow Level Security Fundamentals
pg_cronรัน SQL ตามตารางเวลาข้างใน Postgres ล้วน ๆ ไม่มี scheduler ภายนอกเลย — พิสูจน์แล้วด้วย cron.schedule('name', '* * * * *', $$select 1$$) ที่ยิงจริงและไปโผล่ใน cron.job_run_details ตรงต้นนาทีถัดไปพอดี การเรียก Edge Function ตามตารางเวลาใช้ supabase_functions.http_request(...) แบบเดียวกับที่ trigger ของ Database Webhook ใช้Background Tasks and Webhooks with Functions
pg_graphqlextension ที่ expose schema เดียวกันเป็น GraphQL ที่ /graphql/v1 — ไม่ได้เปิดเป็น default บน local stack นี้ (ต้อง create extension pg_graphql ก่อน) แต่พอเปิดแล้ว บังคับใช้ RLS แบบเดียวกับ REST ทุกประการผ่าน Postgres connection และ role เดียวกัน — พิสูจน์แล้วด้วย table notes สอง user เดียวกัน ที่คืน row ต่างกันให้แต่ละคนที่เรียก GraphQL ไม่มีชั้น authorization แยกสำหรับ GraphQL ให้ตั้งค่าเพิ่มเลยThe REST API (PostgREST)
pg_netextension ของ Postgres ที่ trigger ของ Database Webhook สร้างอยู่บน — พิสูจน์แล้วว่า asynchronous จริง ด้วยเวลาที่วัดได้จริง (insert ใช้เวลา 2.767ms สั้นกว่า HTTP call ที่มันสั่งไปมาก) ผลลัพธ์จริงไปโผล่ทีหลังใน net._http_response อ่านย้อนหลังแทนที่จะ awaitBackground Tasks and Webhooks with Functions
PGRST102”All object keys must match” — error จริงที่ bulk insert ซึ่งมี JSON key ไม่ตรงกันในแต่ละ row จะได้เป็น default แก้ได้ด้วย Prefer: missing=default บวกกับ ?columns= ที่ระบุรายชื่อ column ชัดเจน ซึ่งจะเติม key ที่ row ไหนขาดไปด้วยค่า default ของ column นั้นThe REST API (PostgREST)
PGRST205”Could not find the table … in the schema cache” — 404 จริงที่ PostgREST คืนทั้งกรณี table ไม่มีอยู่จริง และกรณี table ที่ grant ครบถ้วนแต่อยู่ใน schema ที่ไม่เคยถูกเพิ่มเข้า [api] schemas มันบอกว่า “ไม่มีอยู่” ไม่เคยบอกว่า “ต้องห้าม” ไม่ว่ากรณีไหนSecuring the Data API
pgTAP / supabase test dbframework การ test จริงของ Postgres ที่ขับเคลื่อนด้วย extension pgtap — assertion plan(), ok(), is() ข้างใน transaction ที่ rollback ได้ รันต่อไฟล์พร้อม TAP output จริง (ok 1 - ...) หรือสรุปผ่าน supabase test db สำหรับ CI พิสูจน์ด้วย test จริงสอง assertion: หนึ่งเช็คว่า table มี RLS policy ตรงตามจำนวนที่คาดไว้ อีกหนึ่งสลับเป็น set local role anon กลาง test เพื่อยืนยันว่า table ที่ว่างเปล่าจาก RLS จริง ๆ คืน 0 row — ไม่ได้ติดตั้งมา default บนโปรเจกต์ใหม่ (ต้อง create extension pgtap ก่อน)Verification Tools
PITR (point-in-time recovery)กู้คืนโปรเจกต์กลับไปยังจุดเวลาใดก็ได้ภายใน retention window ไม่ใช่แค่ snapshot รายวันล่าสุด — สำคัญมากเวลาที่ incident ที่ต้องแก้เกิดขึ้นหลาย ชั่วโมงหลังจาก backup ล่าสุดถูกทำSecurity Checklist and Production Readiness
PKCE (flowType: 'pkce')auth flow default ของ supabase-js (flow แบบเก่า implicit ยังใช้ได้อยู่แต่ไม่ใช่ default) — magic link หรือ OAuth redirect พา authorization code แบบ ?code= มา ไม่ใช่ token ตรง ๆ และ client ต้องเรียก exchangeCodeForSession(code) เพื่อแปลงเป็น session จริง พิสูจน์ตั้งแต่ต้นจนจบจากอีเมลที่ Mailpit จับได้จริงไปจนถึง session ที่ login สำเร็จAuth in the Client
policy (RLS policy)statement create policy ที่ scope operation หนึ่ง (select/insert/update/delete) บน table หนึ่งไปยังเงื่อนไข using และ/หรือ with check ประเมิน ต่อ row ต่อ operation — policy ที่ขาดไปสำหรับ operation หนึ่งจะปฏิเสธ operation นั้นเป็น default (เงียบ ๆ เป็น DELETE 0 สำหรับ select/delete, ดังเป็น 42501 สำหรับ insert/update) ไม่เคย fallback ไปใช้ policy อื่นแทนRow Level Security Fundamentals
PostgRESTservice ที่แปลง HTTP request ให้กลายเป็น SQL statement เดียว execute ด้วย role ที่ request นั้นถืออยู่ — ไม่บังคับใช้อะไรของตัวเองเลย ทุกการตัดสินใจเรื่องการเข้าถึงเป็นของ grant และ RLS ของ Postgres เอง !inner, count=planned, และ resolution=merge-duplicates ล้วน compile เป็น SQL จริงที่เขียนเองได้ ไม่ใช่ชั้น permission แยกระดับ frameworkThe REST API (PostgREST)
postgres_changesRealtime ที่ไล่ตาม write-ahead log ของ Postgres เองผ่าน logical replication — พิสูจน์แล้วว่าเงียบสนิทสำหรับ table ที่ยังไม่ถูกเพิ่มเข้า publication supabase_realtime (รอจริง 5 วินาที ไม่มี event เลย ไม่มี error เลย) ตรวจ policy select ของ RLS ของแต่ละ subscriber ใหม่ทุก event ทุก subscriber policy เดียวกับที่ PostgREST จะใช้กับ select ธรรมดาRealtime: Postgres Changes
Prefer: headerheader ของ request ที่เปลี่ยนรูปทรง response หรือความหมายของการเขียนของ PostgREST: return=representation (คืน row ที่เปลี่ยนแทนที่จะเป็น 201/204 ว่างเปล่า), resolution=merge-duplicates (เปลี่ยน insert ที่ conflict ให้เป็น upsert ต้องมี ?on_conflict=), missing=default (เติม key ที่ขาดใน bulk insert ด้วยค่า default ของ column), และ tx=rollback — พิสูจน์แล้วว่าไม่มีผลอะไรเลยเงียบ ๆ บน stack นี้ เพราะถูกล็อกไว้หลังการตั้งค่า db-tx-end ที่ฝั่ง server เท่านั้น ซึ่ง default เป็น commit และไม่ให้ client override ได้The REST API (PostgREST)
presenceมุมมองใน memory ของ Realtime ที่ sync กันว่าใครกำลังเชื่อมต่อ channel อยู่และล่าสุด track() อะไรไว้ผ่าน presenceState() — ไม่ได้ผูกกับ table ไหนเลย หายไปทันทีที่ socket disconnect (เป็น event leave อัตโนมัติ) ไม่ใช่ record ที่ query ได้แบบถาวรเลยRealtime Broadcast and Presence
prepare: falseoption ของ driver postgres npm ที่ต้องใช้เมื่อผ่าน Supavisor transaction mode เพราะ prepared statement ที่สร้างบน backend connection ที่ pool ไว้ตัวหนึ่ง จะไม่มีอยู่บน backend ตัวอื่นที่ query ถัดไปไปตกอยู่ — เป็นวิธีแก้ตามเอกสาร ตรงกับ ?pgbouncer=true ของ Prisma เอง สำหรับปัญหาความเข้ากันไม่ได้จริงที่ถูกอ้างอิงไว้Edge Functions and the Database
private channelchannel (config: { private: true }) ที่จำกัดให้เฉพาะ client ที่ authenticated แล้ว โดยส่งการ authorize ผ่าน RLS บน realtime.messages แทนที่จะใช้ logic ของ Realtime เอง realtime.messages มาพร้อม RLS เปิดใช้งานแต่ ไม่มี policy เลย — พิสูจน์แล้วว่าได้ CHANNEL_ERROR จริงเมื่อยังไม่มี policy เลย และต้องเรียก client.realtime.setAuth(token) เองด้วย ต่างจาก postgres_changes ที่ดึง session มาให้อัตโนมัติRealtime Broadcast and Presence
publication (supabase_realtime)publication ของ Postgres ที่ alter publication supabase_realtime add table <t> ต่อสาย table เข้าไป — ขั้นตอน opt-in ขั้นตอนเดียวที่ถ้าไม่ทำ postgres_changes จะไม่ streaming อะไรให้ table นั้นเลย ไม่ว่า code ฝั่ง subscribe จะดูถูกต้องแค่ไหนRealtime: Postgres Changes
publishable key / secret keyตั้งแต่ปลายปี 2025 รูปแบบ key ใหม่กว่า (sb_publishable_.../sb_secret_...) ที่มาแทน JWT anon/service_role แบบเดิม — บทบาทความน่าเชื่อถือเหมือนเดิม (publishable ปลอดภัยพอใส่ฝั่ง client, secret bypass RLS และอยู่ฝั่ง server เท่านั้น) เป็น string สั้นลงไม่ใช่ JWT หมุนเวียน (rotate) ได้โดยไม่ downtime key เดิมยังใช้ได้ต่อProject Setup and the Supabase CLI
Termความหมายบทเรียน
query builder (supabase-js) / .range()ตัวประกอบ URL-และ-header แบบ typed บาง ๆ — ไม่ execute อะไรเลยจนกว่าจะ await พิสูจน์แล้วด้วยการ log query.url.toString() ก่อน await แล้วเห็น request จริงที่กำลังจะยิงออกไป .range() พิสูจน์แล้วว่า ไม่ใช่ HTTP Range header ตามชื่อ — มันส่ง query parameter limit/offset แทน ซึ่งเป็นกลไก pagination ของ PostgREST เองQuerying with the Client SDK
Termความหมายบทเรียน
realtime.messagestable จริงของ Postgres ที่ทุกข้อความของ private channel และ broadcast_changes()/realtime.send() ถูก insert เข้าไป ถูกล็อกด้วย RLS โดยใช้ realtime.topic() (ฟังก์ชันที่แทน “ชื่อ channel ที่ connection นี้กำลังขอ authorize”) แทนการเทียบ column ธรรมดาRealtime Broadcast and Presence
realtime.send()ฟังก์ชันพื้นฐานกว่าที่ broadcast_changes() สร้างอยู่บน — realtime.send(payload, event, topic, private) broadcast JSON อะไรก็ได้โดยไม่มีรูปทรงแบบ row-change และไม่ต้องมี trigger หรือ table เลย argument ตัวที่สี่ตัดสินว่าต้องมี RLS บน realtime.messages หรือไม่ (true) หรือข้ามไปเลย (false ส่งให้ subscriber ใครก็ได้บน topic แบบ public นั้น)Realtime Broadcast and Presence
refresh token rotationการ refresh token ที่สำเร็จทุกครั้งจะคืน refresh token ใหม่ และทำให้ token ที่เพิ่งใช้เป็นโมฆะ refresh_token_reuse_interval (10 วินาทีบน local stack นี้) ยอมให้ token ที่เพิ่งใช้ ก่อนหน้าทันที ใช้ซ้ำได้ภายในช่วงผ่อนผันนี้ (การ retry ของ network จริง ๆ ไม่ควรทำให้เสีย session) ส่วน token ที่เก่าไปสองรุ่นจะได้ error refresh_token_already_used ที่เจาะจงจริง ไม่ใช่ 401 ทั่วไปSessions and JWTs
REPLICA IDENTITYควบคุมว่า row “เก่า” จะถูกเขียนลง WAL มากแค่ไหนสำหรับ UPDATE/DELETE — default มีแค่ primary key พิสูจน์แล้วตรง ๆ: payload.old มีแค่ {"id":...} จนกว่าจะรัน alter table ... replica identity full แล้วหลังจากนั้นมีค่าของทุก column ก่อนถูก updateRealtime: Postgres Changes
RLS (Row Level Security)ไม่เปิดเป็น default เลย — ต้องรัน alter table ... enable row level security เองเสมอ และพอเปิดแล้วแต่ไม่มี policy เลย table จะถูกล็อกสนิท (พิสูจน์แล้ว: ทั้ง anon และ user ที่ signed-in จริงได้ [] เหมือนกัน มีแค่ service_role เท่านั้นที่เห็น row) ไม่ใช่ security model ทั้งหมดในตัวเอง — เป็นด่านที่ สอง จากสองด่าน ประเมินก็ต่อเมื่อ Data API exposure ให้ request ผ่านมาถึง table ได้แล้วเท่านั้นRow Level Security Fundamentals
RPC (/rpc/<function_name>)PostgREST expose Postgres function อัตโนมัติแบบเดียวกับที่ expose table — ไม่มีไฟล์ routes ไม่มีขั้นตอน deploy returns setof/returns table กำหนดรูปทรงที่ curl เห็นจริง RPC แบบ atomic (update ... set count = count + 1 ครั้งเดียว) หลีกเลี่ยง race ของการอ่านแล้วเขียนแบบ client-side ได้Database Functions as RPC
Termความหมายบทเรียน
search_pathลำดับการ resolve ชื่อแบบข้อความล้วน ๆ ที่ Postgres ใช้กับชื่อที่ไม่ qualify อย่าง orders — กลายเป็นรูรั่วความปลอดภัยจริงทันทีที่ function แบบ security definer ปล่อยไว้ไม่ pin: พิสูจน์แล้วด้วย table agapi_attacker.agapi_accounts ที่ปลูกไว้ เข้ามาแย่ง lookup ที่ไม่ qualify แล้วคืนยอดเงินปลอม แก้ได้ทั้งหมดด้วย set search_path = '' บวกกับ qualify ทุก relation ใน body ของ function ให้ครบDatabase Functions as RPC
security definerattribute ของ Postgres function ที่ทำให้รันด้วยสิทธิ์ของ เจ้าของ function แทนที่จะเป็นของผู้เรียก — พิสูจน์แล้วว่าเห็น row ที่ RLS ของผู้เรียกเองจะซ่อนไว้ (admin report ที่ข้าม user ได้) ซึ่งเป็นเหตุผลพอดีที่ต้อง pin search_path และปลอดภัยเท่าที่คนที่ได้รับ execute เท่านั้นDatabase Functions as RPC
security invokerค่า default ของ Postgres function ที่ไม่มี clause security เลย — รันด้วย role ของผู้เรียก ดังนั้น RLS ยังใช้งานเหมือนเดิมทุกประการกับที่ query ตรง ๆ จะเจอ พิสูจน์แล้วด้วย function ที่ body ไม่มี where เลยแม้แต่นิดเดียว แต่ยังคืนแค่ row ของผู้เรียกแต่ละคน เพราะ Postgres ใช้ using ต่อ row ไม่ขึ้นกับ body ของ functionDatabase Functions as RPC
security_invoker (view option, Postgres 15+)ทำให้ create view ประเมิน RLS ด้วยตัวตนของ role ที่ query แทนที่จะเป็นของเจ้าของ view (ที่ RLS เว้นให้) — พิสูจน์แล้ว: view เดียวกันโชว์ user คนที่สองทุก row ก่อนตั้ง option นี้ และโชว์แค่ row ของตัวเองหลังรัน alter view ... set (security_invoker = true) ทุก view ที่ migration สร้างจะใช้พฤติกรรมก่อน 15 เป็น default จนกว่าจะตั้งค่านี้เองRow Level Security Fundamentals
seed.sql (supabase/seed.sql)ชุด row คงที่ที่โหลดอัตโนมัติหลัง supabase db reset (และ db push --include-seed ถ้าเลือก) — สำหรับ fixture ของการพัฒนาในเครื่องและ CI ระบุตรง ๆ ว่าไม่ใช่สิ่งที่ไฟล์ migration จริงควรถืออยู่ เพราะ migration อธิบายประวัติ schema ส่วน seed data ไม่ใช่ทั้งสองอย่างนั้นTables, Schema, and Migrations
service_role key / rolebypass Row Level Security ทั้งหมดผ่าน attribute BYPASSRLS จริงของ Postgres — มีไว้สำหรับ code ฝั่ง server ที่เชื่อถือได้เท่านั้น (Edge Function, process ฝั่ง backend) ห้ามอยู่ใน client bundle เด็ดขาด เพราะใครก็ตามที่ถือมันอ่านและเขียนทุก row ของทุก table ที่ expose ได้หมดProject Setup and the Supabase CLI
session mode (Supavisor, port 5432)ให้ client มี connection Postgres เฉพาะตัวตลอดเวลาที่ยังเชื่อมต่ออยู่ — feature parity เต็มรูปแบบรวมถึง prepared statement แลกกับการถือ pool slot ไว้หนึ่งช่องต่อ client ตลอดเวลา จึงไม่ scale ไปสู่ client แบบสั้น ๆ จำนวนมากได้เท่า transaction modeConnection Pooling and Supavisor
Smart CDNedge caching สำหรับรูปที่ transform แล้ว มีเฉพาะบน hosted เท่านั้น key ด้วยไฟล์และ transform parameter ที่เจาะจง — ตามเอกสาร ใช้เวลาได้ถึงประมาณ 60 วินาทีให้การ invalidate cache หลัง update แพร่กระจายไปทั่ว และใช้ได้กับ request แบบ signed-URL ด้วย ไม่ใช่แค่ public เท่านั้น (มาจากเอกสาร ไม่สามารถ reproduce บน local Docker stack ที่ไม่มี CDN edge)Image Transformations and CDN
sslmode=disablequery parameter ที่ต้องมีบน connection string ของ db push --db-url/pooler ของ local stack นี้ — พิสูจน์สดในการเรียก db push --dry-run จริง — เพราะ container ของ Postgres และ pooler ในเครื่องของ CLI นี้ไม่ terminate TLS แบบที่ connection string ของโปรเจกต์ hosted ทำ การละมันไว้บน connection ในเครื่องทำให้เชื่อมต่อไม่ได้เลยBranching and the Migrations Workflow
stable / immutable / volatileคำสัญญาเรื่อง volatility ของ Postgres function ใช้แค่โดย PostgREST เพื่อตัดสินว่า HTTP verb ไหนปลอดภัย (stable/immutable ปลดล็อก GET ที่ cache ได้) — บังคับใช้ระดับ transaction ไม่ใช่ระดับ route: function ที่ติด label ผิดแล้วพยายามเขียนข้างใน transaction แบบอ่านอย่างเดียวของ GET จะพังด้วย error จริง (25006) แทนที่จะถูกปฏิเสธตั้งแต่แรกด้วยข้อความสุภาพDatabase Functions as RPC
Storage (storage.objects / storage.buckets)ไม่ใช่ระบบ permission แยกต่างหาก — เป็น API บาง ๆ อยู่หน้า blob store ที่เข้ากันได้กับ S3 สำหรับข้อมูล ไบต์ บวกกับ table จริงของ Postgres, storage.objects, สำหรับทุกอย่างเกี่ยวกับมัน (owner, path, bucket, content type) ทุกการตัดสินใจเรื่องการเข้าถึง row เป็น RLS ล้วน ๆ พิสูจน์แล้วด้วย error รูปแบบ 403/42501 เดียวกับ RLS ของ table อื่น ๆStorage Buckets and Policies
storage.foldername()แยก path ของ object ออกเป็น array ของ segment ((storage.foldername(name))[1]) — pattern มาตรฐานคือให้ segment แรกเป็น auth.uid() ของ user ที่ upload เอง ดังนั้น policy ที่เทียบมันจะจำกัด user แต่ละคนให้อยู่ในโฟลเดอร์ของตัวเองเท่านั้น พิสูจน์แล้วสด ๆ ว่า user คนหนึ่งเขียนเข้าโฟลเดอร์ของอีกคนถูกปฏิเสธด้วย error RLS รูปแบบเดียวกับ policy ของ table ธรรมดาStorage Buckets and Policies
StudioGUI บนเว็บ (Table Editor, SQL Editor, Query Performance, Advisors, Database > Replication) สำหรับตั้งค่าโปรเจกต์โดยไม่ต้องเขียนทุกการเปลี่ยนแปลงเอง — การแก้ schema ผ่าน GUI เป็นของจริงและมีผลทันที แต่มองไม่เห็นใน git log จนกว่าจะถูกจับมาเป็น migration ด้วย db diffTables, Schema, and Migrations
@supabase/ssrclient แบบ cookie ที่เชื่อม browser กับ server code (Server Components, Route Handlers, middleware) ที่ client แบบ localStorage ธรรมดาเข้าไม่ถึง — ทั้งสองฝั่งอ่าน/เขียน cookie เดียวกัน และการตั้ง plumbing ของ setAll/cache-header ผิด เป็นสาเหตุที่เอกสารเองอ้างถึงซ้ำ ๆ ว่าทำให้ user ถูก log out แบบสุ่มSessions and JWTs
Supavisorconnection pooler ของ Supabase อยู่หน้าลิมิต connection พร้อมกันของ Postgres (หลักร้อยต้น ๆ เป็น default) — ตรงกับปัญหาที่ traffic ของ serverless/Edge Function สร้างขึ้นพอดี จากการที่แต่ละตัวพยายามเปิด connection ตรงของตัวเองConnection Pooling and Supavisor
Termความหมายบทเรียน
tenant_id claim patterncustom claim (ไม่ใช่ claim default ของ GoTrue — จะเป็น null ใน policy ถ้าไม่มี Auth Hook ใส่ให้) ที่ทำให้ policy แบบ multi-tenant เช็คได้จากการอ่าน JWT ครั้งเดียวแทนที่จะ join ทุก query — พิสูจน์แล้วว่าซ่อน row ของ tenant อื่นได้ถูกต้องเมื่อ hook ต่อสายให้แล้ว และมีผลแค่กับ token ใหม่ เท่านั้น ไม่มีผลย้อนหลังRLS Patterns and Pitfalls
transaction mode (Supavisor, port 6543)ถือ connection ของ Postgres ไว้แค่ระหว่าง transaction เดียวแล้วคืนกลับ pool ทันที — ทำให้ pool เล็ก ๆ รองรับ client แบบสั้น ๆ จำนวนมากได้ (คำแนะนำ default สำหรับ Edge Functions และ serverless) แลกกับการไม่มี feature ระดับ session อย่าง prepared statementConnection Pooling and Supavisor
TUSprotocol การ upload แบบ resume ที่ Storage implement ไว้ที่ /storage/v1/upload/resumable แนะนำให้ใช้เมื่อไฟล์ใหญ่กว่าประมาณ 6MB หรือ connection ไม่เสถียร — พิสูจน์แล้วทีละ chunk ด้วย curl ดิบ ๆ: POST เพื่อสร้าง, PATCH พร้อม Upload-Offset เพื่อส่งไบต์, HEAD เพื่อยืนยันว่า server มีไบต์อยู่กี่ไบต์แล้วหลังจากจำลอง connection หลุดStorage Buckets and Policies
Termความหมายบทเรียน
using / with checkสองด่านแยกกันในสองจังหวะที่ต่างกัน: using คุมว่า row ที่ มีอยู่แล้ว อันไหนถูก target ได้บ้าง (select/update/delete) ล้มเหลวแบบ เงียบ — delete ที่ไม่มี policy match จะได้ DELETE 0 จริง ไม่มี error with check คุมว่า row ที่กำลังจะ กลายเป็น อะไรได้รับอนุญาตไหม (insert/update) ล้มเหลวแบบ ดัง ด้วย 42501 จริง Postgres ใช้ using แทน with check ถ้าไม่ได้ระบุ with check ไว้ ดังนั้น using ที่ scope ด้วย ownership ก็ปลอดภัยอยู่แล้วแม้ไม่มี with check แยกต่างหากเลยRow Level Security Fundamentals
Termความหมายบทเรียน
verify_jwtค่าตั้งค่าต่อ-function (true เป็น default) ที่ปฏิเสธ request ก่อนที่ code ของ function เองจะรันเลย ถ้าไม่มี JWT ของ Supabase ที่ valid — พิสูจน์แล้วด้วย error body จริงที่มีโครงสร้าง (UNAUTHORIZED_NO_AUTH_HEADER, UNAUTHORIZED_INVALID_JWT_FORMAT) ต่างจากอะไรก็ตามที่ code ของ function เองจะสร้างได้ ตรวจแค่ว่า token เป็น JWT ที่เซ็นถูกต้องจริง — anon key ของโปรเจกต์เองผ่านการตรวจนี้ เพราะตัวมันเองก็เป็น JWT ที่ valid verify_jwt = false จำเป็นสำหรับ webhook receiver ที่ไม่มี JWT ของ Supabase ให้ยื่นเลยWriting and Invoking a Function
Termความหมายบทเรียน
WAL (write-ahead log)บันทึกทุกการเขียนของ Postgres เองแบบเรียงลำดับและทนทาน เก็บไว้เพื่อ crash recovery และ replication — เป็น stream เดียวกันเป๊ะ ๆ ที่ postgres_changes ของ Realtime แตะผ่าน logical replication ซึ่งเป็นเหตุผลที่มันสืบทอดคุณสมบัติของ Postgres เอง (ไม่มีอะไร stream จนกว่า table จะอยู่ใน publication; row เก่าจำกัดแค่ replica identity เว้นแต่จะขยายไว้)Realtime: Postgres Changes