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

Combining REST, Realtime, and Storage

feature จริงแทบไม่เคยแตะแค่ service เดียว — upload รูป บันทึก row ที่ reference รูปนั้น แล้วให้ client อื่นเห็นรูปนั้นโผล่ขึ้นมาแบบ live คือ flow เดียวที่ compose Storage, REST API ที่ป้องกันด้วย RLS และ Realtime เข้าด้วยกัน และแต่ละส่วนก็คือสิ่งที่เรียนมาแล้วในบทก่อนหน้า

นี่คือ call supabase.storage.from(bucket).upload() ตัวเดียวกับที่เจอในครึ่ง Storage ของ module Realtime and Storage ฟังก์ชันนี้ส่ง bytes ของ file ไปที่ bucket และถ้าสำเร็จจะคืน path ที่ file ถูกเก็บไว้

const file = fileInput.files[0];
const filePath = `${userId}/${crypto.randomUUID()}-${file.name}`;
const { data: uploadData, error: uploadError } = await supabase.storage
.from('photos')
.upload(filePath, file);
if (uploadError) {
console.error('upload failed:', uploadError.message);
return;
}

การเช็ก error ตรงนี้สำคัญเหมือนในบทก่อน network สะดุดหรือ policy ของ Storage ปฏิเสธ upload ก็โผล่มาผ่าน uploadError เหมือนกัน ไม่ใช่ exception ที่ถูก throw และถ้าข้ามการเช็กนี้ไป ก็เท่ากับพยายาม insert row ที่ชี้ไปยัง file ที่ไม่เคยถูกบันทึกจริงแบบเงียบ ๆ

พอ file ถูกเก็บไว้แล้ว app จะบันทึก row ใน posts ที่ชี้ไปยัง file นั้น นี่คือ call .insert() ธรรมดา แต่สำเร็จได้เพราะ row level security policy จาก module Auth เท่านั้น — policy ที่ให้ user insert row ได้ก็ต่อเมื่อ user_id ตรงกับ auth.uid() ฝั่ง client ไม่ต้องทำอะไรเพิ่มเพื่อให้ได้การป้องกันนั้น เพราะ Postgres บังคับใช้ให้อัตโนมัติ สำหรับทุก request ที่ client ส่งไป

const { data: post, error: insertError } = await supabase
.from('posts')
.insert({
user_id: userId,
photo_path: uploadData.path,
})
.select()
.single();
if (insertError) {
console.error('insert failed:', insertError.message);
return;
}

ถ้า userId ไม่ตรงกับ auth.uid() ของ user ที่ signed in อยู่ RLS จะปฏิเสธ insert และ insertError จะมีค่า — { data, error } pattern เดียวกันใช้ได้ตรงนี้เหมือนกับ upload และกฎเดียวกันจากบทก่อนก็ใช้ได้เช่นกัน branch ตาม insertError.code ถ้าต้องแยกว่าเป็นการถูก policy ปฏิเสธหรือเป็นอย่างอื่น แทนที่จะ parse ข้อความ message

client ตัวที่สองที่ต่ออยู่แล้ว — คนอื่นที่กำลังดู feed เดียวกัน — subscribe อยู่กับ postgres_changes บน table posts ตามที่ module Realtime อธิบายไว้ ทันทีที่ insert จาก step 2 commit Postgres จะ replicate การเปลี่ยนแปลงนั้นออกไป และ subscription นี้ก็จะ fire พร้อม row ใหม่เป็น payload

supabase
.channel('posts-feed')
.on(
'postgres_changes',
{ event: 'INSERT', schema: 'public', table: 'posts' },
(payload) => {
// payload.new is the row just inserted in step 2
addPostToFeed(payload.new);
}
)
.subscribe();

สิ่งนี้ทำงานได้ก็เพราะ posts ถูกเพิ่มเข้าไปใน publication supabase_realtime แล้ว — ขั้นตอน setup จาก module Realtime ถ้าไม่มีขั้นตอนนี้ การเปลี่ยนแปลงของ table จะไม่ถูก replicate ออกมาเลย ไม่ว่า listener .on('postgres_changes', ...) ตัวนี้จะเขียนถูกต้องแค่ไหน insert ใน step 2 ก็จะยังสำเร็จอยู่ดี แต่ callback ตัวนี้จะไม่ fire เลย

ไม่มีอะไรพิเศษที่เป็น integration เฉพาะระหว่าง Storage, REST API และ Realtime — แต่ละ service ทำสิ่งที่ทำใน module ของตัวเองตามปกติ และ client code คือสิ่งที่ผูกทุกอย่างเข้าด้วยกันเป็น feature เดียวที่ user เห็น client ที่ upload ไม่เคยคุยกับ client ที่ subscribe อยู่โดยตรง flow นี้ทำงานได้เพราะทั้งคู่คุยกับ Supabase project เดียวกัน และ replication stream ของ Postgres คือสิ่งที่เชื่อม write ฝั่งหนึ่งเข้ากับ notification อีกฝั่ง

flowchart LR
  upload["storage.from('photos').upload(file)"] --> insert["from('posts').insert({ photo_path, user_id })"]
  insert -->|"allowed only if RLS policy passes"| commit[("Row committed in Postgres")]
  commit -->|"replicated because posts is in supabase_realtime"| notify["postgres_changes INSERT event"]
  notify --> other["Other client's .on('postgres_changes', ...) callback fires"]
One feature, three services: upload, insert under RLS, then a live notification
ใน feature upload รูป storage.from('photos').upload() เป็นของ Supabase service ไหน
ทำไม call .from('posts').insert() ใน step 2 ถึงสำเร็จเฉพาะ user ที่ถูกต้องเท่านั้น
ทำไม postgres_changes subscription ของ client อีกตัวถึงพึ่งพาสิ่งที่มาจาก module Realtime โดยเฉพาะ
ทำไม error handling ยังสำคัญใน flow หลาย step อย่าง upload แล้ว insert แล้ว subscribe