วิธีอ่านโค้ด Supabase ที่ไม่ได้เขียนเอง
ทุกโมดูลก่อนหน้านี้ในคอร์สสอนให้คุณ เขียน schema, policy, client call, edge function และการต่อ realtime/storage โมดูลนี้สอนอีกครึ่งหนึ่ง: การอ่าน migration, การเรียก supabase-js, edge function หรือ config.toml ที่คนอื่น — หรือสิ่งอื่น — เขียนไว้ แล้วตัดสินว่ามันถูกต้องจริงหรือไม่ AI assistant ยื่น table ที่มี policy, client ที่ compile ผ่าน, และ function ที่คืน 200 ตั้งแต่ครั้งแรกให้คุณ และความสำเร็จนั้นแหละคือกับดัก: Postgres ยอมรับ migration, tsc กับ deno check ผ่านทั้งคู่, และ demo ทำงานได้จริง — ในขณะที่โค้ดกลับไม่ให้สิทธิ์ที่ควรให้แบบเงียบ ๆ หรือให้สิทธิ์ทุกคนในสิ่งที่ควรกันไว้ ชั้นต่าง ๆ ของ Supabase เองแทบไม่ตรวจสอบ “เจตนา” เลย: policy for select using (true) ถูกต้องตามไวยากรณ์เป๊ะแต่ผิดความหมายอย่างสิ้นเชิง createClient() ไม่เคยปฏิเสธ call ที่จะพัง 403 ตอน runtime และ security definer compile ผ่านเหมือนกันไม่ว่าจะ pin search_path ไว้หรือไม่
รูปแบบที่พบบ่อยที่สุด พิสูจน์ตลอดทั้ง catalog ของโมดูลนี้: โค้ดที่ทำงานตอนทดสอบด้วย service_role key แล้วคืนค่าว่างเปล่า — ไม่มี error, 200 พร้อม array ว่าง — ตอนทดสอบด้วย anon หรือ JWT ของ user จริง service_role ข้าม Row Level Security ไปทั้งหมด ดังนั้น policy ที่หายไป, grant ที่ขาด หรือ table ที่ไม่อยู่ใน publication จะมองไม่เห็นเลยระหว่างพัฒนา อ่านโค้ด Supabase ตามลำดับด้านล่างนี้โดยเฉพาะ เพราะแต่ละขั้นคือจุดที่ “ตอนลองมันทำงาน” กับ “มันถูกต้อง” แยกออกจากกันแบบเงียบ ๆ — และทุกบทเรียนหลังจากนี้ในคอร์สก็มี section ## Reading AI-generated code ของตัวเองที่เจาะลึกแต่ละชั้นเต็มรูปแบบอยู่แล้ว
flowchart TD
A["1. Schema & RLS<br/>table, policy, using vs with check -- ทุกคำสั่งมี policy ของตัวเองหรือยัง?"] --> B["2. Role & key<br/>anon/authenticated/service_role -- โค้ดนี้ใช้ key ไหนจริง ๆ?"]
B --> C["3. Auth flow<br/>getSession vs getUser, refresh rotation, custom claims -- verify แล้วหรือแค่ decode?"]
C --> D["4. Client calls<br/>เช็ค {data, error} แล้วหรือยัง? singleton? header ถูก freeze?"]
D --> E["5. Edge functions<br/>jsr:/npm: specifier, client ใช้ key ไหน, secrets"]
E --> F["6. Realtime & storage<br/>เป็นสมาชิก publication แล้วหรือยัง, replica identity, policy ที่ scope ตาม folder"]
F --> G["7. Ops & pooling<br/>prepare:false, pg_net เป็น async, ใครถือ key นี้อยู่จริง"]
1. Schema & RLS
หัวข้อที่มีชื่อว่า “1. Schema & RLS”ก่อนอื่นให้อ่านนิยาม table และทุก policy ที่ติดอยู่กับมันก่อน — เพราะ table ที่เปิด RLS แล้วไม่มี policy เลยไม่ใช่ error แต่เป็น table ที่คืนค่าว่างเปล่าให้ทุกคนยกเว้นเจ้าของมันตลอดไป โดยไม่มีคำเตือนจากชั้นไหนเลย
- สิ่งที่ต้องมองหา: ทุก table ที่ต้องการมี
enable row level securityหรือยัง? ทุกคำสั่งที่ client เรียกจริง (select,insert,update,delete) มี policy ของตัวเอง หรือ table นั้นมีแค่ policyselectเพราะเป็นสิ่งแรกที่มีคนทดสอบ? บน policyupdateมีการเขียนwith checkแยกออกมาชัดเจน หรือใช้using (true)ทำหน้าที่ทั้งสองอย่าง — รูปแบบเดียวกับที่เปิดช่องให้ user คนหนึ่งเปลี่ยนความเป็นเจ้าของ row ของอีกคนได้? ฟังก์ชัน RPC ที่เป็นsecurity definerpinset search_path = ''หรือเปล่า หรือเชื่อsearch_pathของ session ผู้เรียกสำหรับการอ้างอิง table แบบไม่ qualify schema? - คำถามที่ต้องถาม: ถ้า policy นี้ถูก deploy จริง แล้ว user ทั่วไปที่ใช้แอปนี้ ไม่ต้อง spoof อะไรเลย — แค่เปลี่ยนค่า field ที่ request เดิมยอมให้ส่งอยู่แล้ว — อะไรที่หยุดไม่ให้การเขียนนั้นไปตกที่ row ของคนอื่น?
- บทเรียนที่อธิบายเรื่องนี้: กลุ่ม Schema & migrations กับ RLS & roles ของ AI Bug Catalog (entry 1–8) สำหรับ
42501จริง, row ที่ถูกขโมยจริง, และการรั่วไหลของsecurity definerที่ถูก schema-injection จริง; Row Level Security Fundamentals และ RLS Patterns and Pitfalls สำหรับกลไกทั้งสองแบบเต็มรูปแบบ
2. Role & key
หัวข้อที่มีชื่อว่า “2. Role & key”เมื่อเข้าใจ schema กับ policy แล้ว ให้ตรวจว่าโค้ดตรงหน้าใช้ key ไหนใน 3 ตัวจริง ๆ — เพราะ anon, authenticated, และ service_role ไม่ใช่สิ่งที่ใช้แทนกันได้ และ service_role ข้าม policy ทุกตัวจากขั้นที่ 1 โดยไม่บอกอะไรใน response เลย
- สิ่งที่ต้องมองหา: มีการอ้างอิง
SERVICE_ROLE_KEY(หรือSUPABASE_SERVICE_ROLE_KEY) ในที่ที่ browser bundle อาจส่งออกไปได้หรือไม่ — client component, repo สาธารณะ, ไฟล์.envที่วางอยู่ในsupabase/functions/<name>/โดยไม่มี suffix.local(.gitignoreของ CLI เองกันแค่.env.local/.env.*.localไม่ใช่.envเปล่า ๆ)? demo ที่ “ทำงานได้” ใช้service_roleเพราะ policy RLS จริงพังอยู่แล้วแต่ไม่มีใครสังเกตหรือเปล่า? - คำถามที่ต้องถาม: ถ้า request นี้ถูกยิงซ้ำด้วย
anonkey แทน key ที่โค้ดใช้ทดสอบจริง มันยังคืนข้อมูลตามที่อ้างหรือไม่ — หรือความสำเร็จของ demo ขึ้นอยู่กับ key ที่ไม่ควรมาถึง code path นี้เลยทั้งหมด? - บทเรียนที่อธิบายเรื่องนี้: กลุ่ม Ops & pooling ของ AI Bug Catalog (entry 22) สำหรับ response จริงที่เทียบกันตรง ๆ ระหว่าง
anonกับservice_role; Securing the Data API และ Project Setup and the Supabase CLI สำหรับโมเดล role/grant เต็มรูปแบบ
3. Auth flow
หัวข้อที่มีชื่อว่า “3. Auth flow”อ่านทุกจุดที่ session หรือ claim ถูกเชื่อถือ — เพราะ getSession() decode JWT ในเครื่องโดยไม่เช็ค signature เลย และโค้ดที่ปฏิบัติต่อค่าที่คืนมาเหมือนเป็น identity ที่ verify แล้วก็แค่เชื่ออะไรก็ตามที่อยู่ใน storage ตอนนั้น
- สิ่งที่ต้องมองหา: authorization check ฝั่ง server เรียก
getSession()(หรืออ่านreq.cookiesแล้ว decode เองด้วยมือ) แทนที่จะเป็นgetUser()/getClaims()หรือเปล่า? refresh-token retry logic สมมติว่าการ reuse ทุกครั้งคือการโจมตีหรือเปล่า ทั้งที่มี grace window สั้น ๆ ที่ปรับได้ (refresh_token_reuse_interval) ที่ยอม token รุ่นก่อนหน้าทันทีโดยตั้งใจอยู่แล้ว? policy หรือ handler อ่าน custom JWT claim (request.jwt.claims ->> 'user_role') ที่ไม่มี Auth Hook ไหนในconfig.tomlของโปรเจกต์นี้เคยถูกเขียนขึ้นมาเพื่อเพิ่มมันเข้าไปเลยหรือเปล่า? - คำถามที่ต้องถาม: ถ้า JWT ที่โค้ดนี้กำลังจะเชื่อถูกแก้ payload ด้วยมือ — header เดิม, signature เดิม (ที่ตอนนี้ใช้ไม่ได้แล้ว) — มีอะไรใน code path นี้ที่สังเกตเห็นจริง ๆ ก่อนที่ claim ข้างในจะถูกนำไปใช้หรือไม่?
- บทเรียนที่อธิบายเรื่องนี้: กลุ่ม Auth & sessions ของ AI Bug Catalog (entry 9–11) สำหรับการพิสูจน์ token ที่ถูกแก้จริง และ
refresh_token_already_usedจริง; Sessions and JWTs สำหรับกลไกทั้งสองแบบเต็มรูปแบบ
4. Client calls
หัวข้อที่มีชื่อว่า “4. Client calls”supabase-js ไม่เคย throw เมื่อ database หรือ API error — มันคืน { data, error } — ดังนั้นโค้ดที่ destructure แค่ data ได้ลบสัญญาณเดียวที่บอกว่า query ล้มเหลวไปแบบเงียบ ๆ
- สิ่งที่ต้องมองหา: เช็ค
errorหรือเปล่า หรือโค้ดกระโดดไปที่data.map(...)ตรง ๆ แล้ว crash ด้วยTypeErrorที่งงกว่าPGRST205/42501/อะไรก็ตามที่เกิดขึ้นจริงมาก? branch logic ใช้error.message(string ที่เปลี่ยนได้ระหว่าง version) แทนที่จะเป็นerror.codeที่เสถียรกว่าหรือเปล่า?.single()ถูกเรียกกับ query ที่คืนได้ 0 หรือหลาย row โดยไม่มีการจัดการPGRST116หรือเปล่า? มีการสร้างcreateClient()ใหม่ต่อ request หรือต่อ component แทนที่จะเป็น singleton ตัวเดียวที่ใช้ร่วมกันหรือเปล่า?global.headers.Authorizationถูกตั้งตอนสร้าง client จนไปทับauth.setSession()ทุกครั้งที่เรียกทีหลังสำหรับ client instance นั้นแบบเงียบ ๆ หรือเปล่า? - คำถามที่ต้องถาม: ถ้า call นี้ล้มเหลว — ชื่อ table ผิด, grant หาย, session หมดอายุ, อะไรก็ได้ — โค้ดที่รันต่อจากนั้นทำงานต่างจากตอนสำเร็จหรือไม่ หรือแค่ crash ต่อไปอีกบรรทัดด้วย error ที่แย่กว่าที่ Postgres ให้มาจริง ๆ?
- บทเรียนที่อธิบายเรื่องนี้: กลุ่ม Client calls ของ AI Bug Catalog (entry 12–15) สำหรับการ crash-on-
nullจริง,PGRST116จริง, คำเตือน singleton จริง, และการพิสูจน์ header ที่ถูก freeze จริง; The supabase-js Client และ Error Handling and Typed Queries สำหรับกลไกทั้งสองแบบเต็มรูปแบบ
5. Edge functions
หัวข้อที่มีชื่อว่า “5. Edge functions”Edge Function รันบน Deno ไม่ใช่ Node — bare import specifier ที่ resolve ได้ปกติในทุกไฟล์อื่นของ repo เดียวกันจะพังทันทีที่ deploy ที่นี่ และรูปแบบความล้มเหลวคือ 503 โดยไม่มีอะไรในโค้ดเองบอกว่าอะไรผิด
- สิ่งที่ต้องมองหา:
importใช้ bare specifier (@supabase/supabase-js) แทนที่จะเป็นjsr:/npm:/relative path หรือเปล่า?createClient()ข้างใน function ใช้ key ไหนจริง ๆ —SUPABASE_ANON_KEY(โดน RLS แบบเดียวกับ anon caller ทั่วไปทุกประการ พิสูจน์แล้วในขั้นที่ 1) หรือSUPABASE_SERVICE_ROLE_KEY(ตัวเดียวที่ทำตัวเหมือน “โค้ดฝั่ง server ที่ข้าม RLS” จริง)? secret อยู่ในไฟล์supabase/functions/<name>/.envธรรมดา แทนที่จะเป็นsupabase secrets set(hosted) หรือไฟล์ที่มี suffix.localชัดเจน (รูปแบบเดียวที่.gitignoreของ CLI นี้ป้องกันจริง) หรือเปล่า? - คำถามที่ต้องถาม: ไฟล์นี้เมื่อเช็คด้วย
deno check— ไม่ใช่tsc, ไม่ใช่ type-checker ของ Node — resolve ทุก import ที่ประกาศไว้จริงหรือไม่ และ client ที่มันสร้างถือ key ที่งานของ function นี้ต้องการจริง ๆ หรือเปล่า? - บทเรียนที่อธิบายเรื่องนี้: กลุ่ม Edge functions ของ AI Bug Catalog (entry 16–18) สำหรับ
BOOT_ERRORจริง,[]ที่ถูก RLS กันแบบ anon จากข้างใน function จริง, และช่องโหว่.gitignoreจริง; Writing and Invoking a Function และ Edge Functions and the Database สำหรับกลไกทั้งสองแบบเต็มรูปแบบ
6. Realtime & storage
หัวข้อที่มีชื่อว่า “6. Realtime & storage”ทั้งสองอย่างนี้คือ Postgres ธรรมดาข้างใต้ — row ของ table หนึ่ง (realtime.messages) และ table ที่มี policy คุ้มกัน (storage.objects) — ดังนั้นนิสัยการอ่าน schema/RLS จากขั้นที่ 1 ใช้ได้เหมือนกันเป๊ะ บวกกับเงื่อนไขเบื้องต้นอีก 2 อย่างที่ไม่มีใครประกาศไว้และพลาดง่าย
- สิ่งที่ต้องมองหา: table ที่
postgres_changessubscription เล็งไปหาเป็นสมาชิกของ publicationsupabase_realtimeจริงหรือไม่ — ตรวจสอบได้ แต่ไม่ได้เป็นไปเองแค่เพราะ table มีอยู่?replica identityเป็นfullหรือ payloadoldของUPDATE/DELETEหดเหลือแค่ primary key (หรือหายไปเลย) แบบเงียบ ๆ? channel แบบ private มี policy ที่ตรงกับ topic ของมันเป๊ะ ๆ บนrealtime.messagesหรือเปล่า หรือ subscription แค่ fail ด้วยUnauthorized? policy ของ storage เช็ค(storage.foldername(name))[1]เทียบกับ id ของผู้เรียกเอง หรือเช็คแค่bucket_id— ปล่อยให้ authenticated user ทุกคนเขียนเข้าไปใน folder ของคนอื่นได้อย่างอิสระ? การ upload ที่มีupsert: trueมี policy select เพิ่มจาก insert/update หรือเปล่า — เส้นทาง upsert ของ storage เช็คว่า object มีอยู่แล้วก่อนเป็นอันดับแรก และ policy select ที่ขาดไปจะทำให้เช็คนั้นล้มเหลว แม้ insert กับ update จะ scope ถูกต้องแล้วก็ตาม? - คำถามที่ต้องถาม: ถ้า table/bucket/channel นี้ถูก query ตรง ๆ กับ Postgres เดี๋ยวนี้เลย —
select * from pg_publication_tables,select relreplident,select policyname, qual from pg_policies— สิ่งที่ตั้งค่าไว้จริงตรงกับสิ่งที่โค้ดตรงหน้าสมมติว่าเป็นจริงอยู่แล้วหรือไม่? - บทเรียนที่อธิบายเรื่องนี้: กลุ่ม Realtime กับ Storage ของ AI Bug Catalog (entry 19–24) สำหรับการพลาด publication แบบเงียบจริง, การถูกปฏิเสธ private-channel จริง,
old: nullจริง, และการเขียนข้าม folder ของคนอื่นจริง; Realtime Postgres Changes, Realtime Broadcast and Presence, และ Storage Buckets and Policies สำหรับทุกกลไกเต็มรูปแบบ
7. Ops & pooling
หัวข้อที่มีชื่อว่า “7. Ops & pooling”ชั้นสุดท้ายคือทุกอย่างเกี่ยวกับ วิธีที่ request ไปถึง Postgres มากกว่าสิ่งที่มันขอ — โหมดของ connection pooling, background job, และ key ตัวไหนไปฝังอยู่ตรงไหนจริง ๆ — และเป็นชั้นที่โค้ดที่ AI เขียนแทบไม่พูดถึงเลย
- สิ่งที่ต้องมองหา: โค้ดที่เชื่อมผ่าน pooler โหมด transaction ของ Supavisor สมมติว่า named prepared statement ทำงานแบบเดียวกับ direct connection หรือเปล่า (
prepare: falseคือทางแก้ที่มีเอกสารรองรับ ไม่ใช่แค่ความชอบส่วนตัวเรื่อง style)? การเรียกpg_netอ่านnet._http_responseใน statement เดียวกับที่ยิง request ออกไป สมมติว่ามันเป็น synchronous round trip ทั้งที่ตามการออกแบบมันคือ background worker แบบ asynchronous หรือเปล่า?db pushถูกรันกับ connection string ที่ผ่าน pooler โดยไม่มีsslmode=disableทั้งที่ pooler ในเครื่องของ CLI นี้ไม่ terminate TLS หรือเปล่า? - คำถามที่ต้องถาม: ความถูกต้องของโค้ดนี้ขึ้นอยู่กับการที่ request เสร็จแบบ synchronous หรือขึ้นอยู่กับการรับประกันระดับ session (prepared statement,
SET LOCAL) ที่อยู่รอดเกินกว่า 1 transaction ที่โหมด transaction ของ Supavisor รับประกันไว้จริง ๆ? - บทเรียนที่อธิบายเรื่องนี้: กลุ่ม Ops & pooling ของ AI Bug Catalog (entry 25–27) สำหรับ race condition ของ
net._http_responseจริง และความล้มเหลวของ prepared statement ที่มีเอกสารรองรับ (และบน local stack นี้จริง ๆ ก็รายงานตรง ๆ ว่า reproduce ไม่ได้); Connection Pooling and Supavisor สำหรับเรื่อง session-vs-transaction mode เต็มรูปแบบ
โมดูลนี้มีอะไรต่อ
หัวข้อที่มีชื่อว่า “โมดูลนี้มีอะไรต่อ”- The AI Bug Catalog — 27 รูปแบบที่เจาะจง แต่ละอันมีโค้ดที่ AI มักเขียน, คำสั่งจริงพร้อมผลลัพธ์จริงที่จับสด ๆ กับ local stack ของโมดูลนี้เอง, และวิธีแก้
- Review Checklist — 27 รูปแบบเดียวกันในรูปแบบ checklist หน้าเดียว 8 กลุ่ม แต่ละบรรทัดลิงก์ไปยังบทเรียนที่พิสูจน์มัน
- Verification Tools —
supabase db lint, query ตรวจ coverage ของ RLS,pg_policies,explain analyzeภายใต้ JWT จริง,supabase test db(pgTAP),supabase gen types+tsc,deno check,curlด้วย anon เทียบ service key, MCP server, Studio, และ harness ของคอร์สนี้เอง แต่ละตัวพิสูจน์อะไรได้บ้างจริง ๆ - ทุกบทเรียนก่อนหน้าในคอร์สนี้มี section
## Reading AI-generated codeของตัวเองพร้อม<SpotTheBug>อยู่แล้ว — catalog ของโมดูลนี้ไม่ได้ซ้ำสิ่งเหล่านั้น แต่จัดกลุ่ม 27 รูปแบบที่กว้างกว่าตามลำดับการอ่านด้านบน เริ่มจาก What Is Supabase ถ้าต้องการเส้นทางบทเรียนแบบเต็มทีละบทแทน
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”Reading order for Supabase code you did not write:
1. Schema & RLS -> does every command (select/insert/update/delete) have its own policy?2. Roles & keys -> which of anon/authenticated/service_role is this code actually using?3. Auth flow -> getSession (decoded, unverified) or getUser (verified)?4. Client calls -> {data, error} checked? singleton? frozen Authorization header?5. Edge functions -> jsr:/npm: specifiers, which key, which secrets file6. Realtime/storage -> publication membership, replica identity, folder-scoped policies7. Ops & pooling -> prepare:false, pg_net is async, which key actually reached this code
"It returned data when I tested it" answers almost none of these -- the single most commonfailure in this catalog is code that works with service_role and is silently empty with anon.ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “ถ้า Postgres ยอมรับ migration แล้ว
tsc/deno checkผ่านทั้งคู่ โค้ดนั้นถูกต้อง” พิสูจน์แล้วตลอดทั้ง catalog ของโมดูลนี้:for update using (true)โดยไม่มีwith checkแยกชัดเจนคือ SQL ที่ถูกต้องตามไวยากรณ์เป๊ะ และเป็นช่องโหว่ row-hijack จริง พิสูจน์สดในบท RLS ของคอร์สนี้เอง — ไม่มี compiler หรือ linter ตัวไหนจับได้ เพราะมันไม่ใช่ syntax error - “policy ที่หายไปจะ throw error แน่นอน ฉันต้องสังเกตเห็น” พิสูจน์แล้วว่าไม่จริง: RLS ที่เปิดแล้วไม่มี policy เลยคืน
200พร้อม[]— ไม่มี error จากชั้นไหนเลย ความเงียบนั่นแหละคือตัว bug - “
service_roleก็แค่anonkey ที่แรงกว่า” มันคือคนละสิ่งกันเลย: มันข้าม Row Level Security ไปทั้งหมด ซึ่งหมายความว่า policy ทุกตัวที่ขั้นที่ 1 ของโมดูลนี้ขอให้อ่านจะมองไม่เห็นเลยระหว่างทดสอบด้วยมัน โค้ดที่ “ทำงานได้” ระหว่างพัฒนาแล้วพังทันทีที่ session ของ user จริงมาแทนที่service_roleคือรูปแบบที่พบบ่อยที่สุดที่ catalog นี้บันทึกไว้ - “โค้ด Supabase ที่ AI เขียนแค่ต้องการ prompt ที่ใหม่กว่า” ปัญหาไม่ใช่แค่ความล้าสมัย:
for select using (true)เป็น SQL ที่ถูกต้อง เป็นปัจจุบัน และถูกต้องตามไวยากรณ์เป๊ะ ที่แค่ไม่ได้มีความหมายตามที่ผู้อ่านซึ่งข้ามขั้นที่ 1 ไปสมมติ — ไม่มี training-data cutoff ไหนแก้นิสัยการอ่านได้