สร้าง Webhook ด้วย Supabase Edge Functions แล้วเก็บทุก Event ลง Postgres ทันที

สรุปสั้น ๆ
Supabase Edge Functions รันบน Deno runtime และอยู่ใกล้ Postgres project ของตัวเองโดยตรง ทำให้เขียน webhook receiver ที่ตรวจ signature แล้วบันทึกข้อมูลลง Postgres ได้ในขั้นตอนเดียวโดยไม่ต้องผ่าน service กลาง แต่ต้องเข้าใจว่า Deno runtime มีวิธีจัดการ dependency และ secret ต่างจาก Node หรือ Cloudflare Workers และควรใช้ unique constraint ระดับฐานข้อมูลช่วยกัน event ซ้ำแทนการพึ่ง in-memory cache อย่างเดียว
Supabase Edge Functions เป็นอีกทางเลือกสำหรับสร้าง webhook receiver นอกเหนือจาก Cloudflare Workers เพราะ Edge Functions ผูกอยู่กับ Supabase project โดยตรง ทำให้การเขียนข้อมูลกลับลง Postgres หลังรับ event ทำได้ในโค้ดชุดเดียวกัน ไม่ต้องแยก service หรือผ่าน REST API ชั้นเพิ่มเติมแบบที่ Workers ต้องทำ
แต่ความสะดวกนี้มาพร้อมรายละเอียดที่ต้องเข้าใจให้ถูก เพราะ Edge Functions รันบน Deno ซึ่งมีวิธีจัดการ import module, secret และ deployment ที่ต่างจาก Node.js หรือ Workers runtime ของ Cloudflare ทีมที่คุ้นเคยกับ ecosystem หนึ่งแล้วย้ายมาเขียน Edge Functions ครั้งแรกมักเจอความสับสนเรื่อง syntax การ import และการตั้งค่า environment variable ที่ไม่เหมือนที่คุ้นเคย
บทความนี้จะอธิบายวิธีสร้าง webhook receiver ด้วย Supabase Edge Functions ตั้งแต่ตรวจ signature ไปจนถึงบันทึกข้อมูลลง Postgres พร้อมชี้จุดที่ทำให้ event ซ้ำหรือข้อมูลผิดพลาดถ้าออกแบบไม่รอบคอบ
Deno Runtime ของ Edge Functions ต่างจาก Node และ Workers ตรงไหน
Edge Functions ของ Supabase รันบน Deno ซึ่งเป็น runtime ที่ออกแบบมาให้ปลอดภัยกว่า Node ในหลายด้าน เช่นไม่ให้สิทธิ์เข้าถึง filesystem หรือ network โดยอัตโนมัติ ต้องประกาศสิทธิ์ชัดเจน และรองรับ TypeScript โดยไม่ต้องผ่านขั้นตอน compile แยกต่างหากเหมือนโปรเจกต์ Node ทั่วไป
การ import module ใน Deno ใช้ URL หรือ npm specifier ตรง ๆ แทนการติดตั้งผ่าน package manager แบบ Node ทำให้ไม่มีไฟล์ node_modules แบบเดิม บาง library ของ Node ที่พึ่งพา native binding เฉพาะแพลตฟอร์มอาจใช้ไม่ได้ตรง ๆ บน Deno เช่นเดียวกับข้อจำกัดที่ Cloudflare Workers เจอ แต่ Deno รองรับ npm package จำนวนมากกว่าผ่าน compatibility layer ที่ค่อนข้างครอบคลุม
ต่างจาก Cloudflare Workers ที่ไม่มี TCP socket API ทำให้ต้องต่อ Postgres ผ่าน REST API หรือ Hyperdrive ตามที่อธิบายไว้ในบทความ Cloudflare Workers เชื่อม Supabase Postgres นั้น Deno runtime ของ Edge Functions รองรับการต่อ TCP ได้ในระดับหนึ่ง ทำให้สามารถใช้ Postgres client เชื่อมต่อฐานข้อมูลของ project ตัวเองได้โดยตรงมากกว่า แต่ก็ยังควรใช้ pooled connection แทน direct connection เพื่อความปลอดภัยด้านจำนวน connection เช่นเดียวกับ pattern ที่ใช้บน serverless platform อื่น
ตรวจ Signature บน Deno ด้วย Web Crypto API เหมือนเดิมแต่มีจุดต่าง
หลักการตรวจ signature ของ webhook บน Edge Functions ไม่ต่างจากแนวคิดทั่วไป คือคำนวณ HMAC จาก raw body ด้วย secret ที่ผู้ให้บริการมอบให้ แล้วเทียบกับ signature ที่แนบมาใน header ก่อนเชื่อ payload ซึ่ง Deno รองรับ Web Crypto API เหมือนกับ Workers ทำให้เขียนโค้ดคำนวณ HMAC ได้โดยไม่ต้องพึ่ง library ภายนอกเช่นกัน
จุดต่างที่ต้องระวังคือการอ่าน request body ใน Deno ต้องแน่ใจว่าอ่านเป็น stream ดิบก่อนแปลงเป็น JSON เช่นเดียวกับ Workers เพราะถ้า framework หรือ router ที่ใช้ parse body เป็น object ให้อัตโนมัติก่อนถึง handler ของเรา อาจทำให้ไม่มีโอกาสเข้าถึง raw body ต้นฉบับมาคำนวณ signature ได้อีก ควรเลือก router หรือเขียน handler เองแบบที่ควบคุมลำดับการอ่าน body ได้ชัดเจน
secret ที่ใช้ตรวจ signature ควรเก็บผ่านระบบจัดการ secret ของ Supabase (ตั้งผ่าน CLI หรือ dashboard) ไม่ฝังในโค้ดที่ deploy ขึ้นไป และไม่ใช้ secret เดียวกับ service role key ของ Supabase เอง เพราะเป็นคนละวัตถุประสงค์ ถ้า secret สำหรับตรวจ webhook หลุดออกไป ผลกระทบควรจำกัดอยู่แค่การปลอมแปลง webhook เท่านั้น ไม่ใช่เปิดช่องให้เข้าถึงฐานข้อมูลทั้งหมด
ทีมที่ต้องรองรับ webhook จากผู้ให้บริการหลายรายพร้อมกันในฟังก์ชันเดียว ควรแยก secret สำหรับตรวจ signature ของแต่ละผู้ให้บริการออกจากกันให้ชัดเจน ไม่ใช้ secret ตัวเดียวกันเทียบกับ payload จากทุกแหล่ง เพราะถ้า provider หนึ่งเปลี่ยนวิธีคำนวณ signature หรือ secret รั่วไหล ผลกระทบจะไม่ลามไปกระทบการตรวจสอบ webhook จาก provider อื่นที่ยังทำงานปกติอยู่ การตั้งชื่อ secret ให้สื่อว่าเป็นของ provider ไหนตั้งแต่ต้นก็ช่วยลดความสับสนตอน debug ในภายหลังเช่นกัน
กัน Event ซ้ำด้วย Unique Constraint ระดับฐานข้อมูล แทนการพึ่ง Cache
เพราะ Edge Functions เขียนข้อมูลลง Postgres ของตัวเองได้โดยตรง แนวทางกัน event ซ้ำที่เหมาะกับสถานการณ์นี้มากกว่าการเก็บสถานะไว้ใน cache ภายนอก คือใช้ unique constraint ที่ระดับตารางฐานข้อมูลเอง สร้างตาราง event log ที่มี column เก็บ identifier เฉพาะตัวของ event พร้อมตั้ง unique constraint ไว้ที่ column นั้น
เมื่อ Edge Function พยายามบันทึก event ที่มี identifier ซ้ำกับที่เคยบันทึกไปแล้ว Postgres จะปฏิเสธการเขียนซ้ำโดยอัตโนมัติผ่าน constraint violation แทนที่ต้องเขียนโค้ดตรวจสอบเองทุกครั้งก่อน insert ทำให้ลดโอกาสเกิด race condition ตอนมี retry สอง request เข้ามาพร้อมกันในเวลาใกล้เคียงกัน ซึ่งเป็นข้อดีที่การเก็บสถานะแบบ key-value ธรรมดาให้ได้ไม่เท่ากับการพึ่งพา constraint ของฐานข้อมูลเอง
แนวทางที่ใช้ได้จริงคือใช้คำสั่ง insert แบบ 'on conflict do nothing' หรือเทียบเท่า แล้วตรวจจากจำนวนแถวที่ถูกเขียนจริงว่าเป็น event ใหม่หรือ event ซ้ำ ถ้าเขียนสำเร็จแสดงว่าเป็น event ใหม่ให้ประมวลผลต่อ ถ้าไม่มีแถวถูกเขียนเพราะชน constraint แสดงว่าเป็น event ซ้ำ ให้ตอบ response สำเร็จกลับไปโดยไม่ทำงานซ้ำ วิธีนี้ปลอดภัยกว่าการตรวจสอบก่อนแล้วค่อย insert แยกสองขั้นตอน เพราะช่วงเวลาระหว่างสองขั้นตอนนั้นอาจมี request อื่นแทรกเข้ามาได้
ทดสอบ Local ด้วย Supabase CLI ต่างจากพฤติกรรมตอน Deploy จริงตรงไหน
Supabase CLI มีคำสั่งสำหรับรัน Edge Functions ในเครื่องคู่กับ Postgres จำลองที่รันผ่าน Docker ทำให้ทดสอบ logic ส่วนใหญ่ได้ใกล้เคียงของจริง แต่มีความต่างสำคัญบางจุดที่ควรรู้ก่อนเชื่อผลทดสอบในเครื่องทั้งหมด
ความต่างแรกคือ secret ที่ตั้งไว้สำหรับรัน local ผ่านไฟล์ env เฉพาะของ CLI ไม่ได้ถูกส่งขึ้นไปที่ project จริงบน cloud โดยอัตโนมัติ ต้องตั้ง secret ของ production แยกต่างหากผ่านคำสั่งเฉพาะก่อน deploy เสมอ ทีมที่ลืมขั้นตอนนี้มักเจอ Edge Function ที่รันได้ปกติตอน local แต่ error ทันทีที่ deploy เพราะหา secret ไม่เจอ
ความต่างที่สองคือพฤติกรรมเรื่อง cold start และ latency ตอน local ที่รันบนเครื่องเดียวแทบไม่มี cold start ให้เห็น เพราะ process ถูกรันค้างไว้ต่อเนื่อง แต่ตอน deploy จริงบน edge network Edge Function อาจถูกสร้าง instance ใหม่เมื่อไม่มี request เข้ามาสักพัก ทำให้ request แรกหลังจากนั้นช้ากว่าปกติเล็กน้อย ถ้า webhook receiver ต้องตอบเร็วภายในเวลาจำกัดที่ผู้ให้บริการกำหนด ควรทดสอบพฤติกรรมนี้บน environment ที่ deploy จริงก่อนเชื่อว่าจะทันเวลาเสมอ
อีกจุดที่ทีมมักมองข้ามคือขั้นตอน deploy ของ Edge Functions ผ่าน CLI เป็นการ push ทั้งฟังก์ชันขึ้นไปแทนที่เวอร์ชันก่อนหน้าทันที ไม่มีขั้นตอน canary หรือ gradual rollout ในตัวแบบที่บาง platform serverless มีให้ ทีมที่แก้ signature verification หรือ logic กัน event ซ้ำแล้ว deploy ตรง ๆ โดยไม่ทดสอบบน staging project แยกก่อน มีความเสี่ยงที่จะทำให้ webhook ที่กำลังทำงานอยู่จริงหยุดรับ event ชั่วขณะระหว่าง deploy หรือแย่กว่านั้นคือ deploy โค้ดที่มี bug ทับเวอร์ชันที่ใช้งานได้ปกติไปเลย แนวทางที่ปลอดภัยกว่าคือมี Supabase project แยกสำหรับ staging ทดสอบ Edge Function เวอร์ชันใหม่ให้ผ่านก่อน แล้วค่อย deploy เข้า production จริงตามขั้นตอนที่ตรวจสอบซ้ำแล้ว
ตารางเทียบวิธีบันทึก Event ระหว่าง Edge Functions กับ Workers
เพื่อให้เห็นความต่างชัดเจน นี่คือตารางเทียบแนวทางบันทึก event ของ webhook ระหว่าง Supabase Edge Functions กับ Cloudflare Workers ที่อธิบายไว้ในบทความ สร้าง Webhook Receiver ด้วย Cloudflare Workers
| ประเด็น | Supabase Edge Functions | Cloudflare Workers |
|---|---|---|
| ระยะห่างจาก Postgres | อยู่ใกล้ project ตัวเอง เขียนตรงได้สะดวก | ต้องผ่าน REST API หรือ Hyperdrive เสมอ |
| กลไกกัน event ซ้ำที่เหมาะสม | unique constraint ระดับฐานข้อมูล | KV เก็บสถานะแบบ key-value |
| Runtime | Deno รองรับ TCP ได้ในระดับหนึ่ง | V8 isolate ไม่มี TCP socket API |
| Edge network coverage | ผูกกับ region ของ Supabase project | กระจายกว้างกว่า เป็นจุดแข็งของ Cloudflare |
ตัวอย่างสมมติ: Event ซ้ำเพราะตรวจสอบก่อน Insert แยกสองขั้นตอน
ลองดูตัวอย่างสมมติของทีมที่สร้าง Edge Function รับ webhook จากระบบแจ้งเตือนภายนอก โดยตอนออกแบบแรกเขียนโค้ดตรวจสอบก่อนว่า identifier ของ event เคยมีอยู่ในตารางหรือยัง ถ้ายังไม่มีค่อยสั่ง insert แยกเป็นสองคำสั่งต่อกัน (ตัวอย่างสมมติเพื่อประกอบการอธิบาย)
ช่วงที่ผู้ให้บริการภายนอก retry สอง request เข้ามาเกือบพร้อมกันเพราะ network หน่วงเล็กน้อย ทั้งสอง request ตรวจสอบพร้อมกันแล้วเห็นว่ายังไม่มี identifier นี้อยู่ในตารางเหมือนกัน เพราะยังไม่มี request ไหน insert เสร็จก่อน ทำให้ทั้งสอง request สั่ง insert เข้าไปพร้อมกัน กลายเป็นมีแถวข้อมูลซ้ำสองแถวสำหรับ event เดียวกันในฐานข้อมูล
ทีมแก้ปัญหาด้วยการเปลี่ยนมาใช้ unique constraint ที่ column identifier พร้อมใช้คำสั่ง insert แบบ on conflict do nothing แทนการตรวจสอบก่อนแยกขั้นตอน เมื่อ request ที่สองพยายาม insert identifier ซ้ำ Postgres จะปฏิเสธแถวนั้นโดยอัตโนมัติ ทำให้เหลือแถวข้อมูลที่ถูกต้องเพียงแถวเดียวแม้จะมี retry เข้ามาพร้อมกันหลายรอบก็ตาม
บทเรียนจากเคสสมมตินี้คือการตรวจสอบก่อนแล้วค่อย insert แยกสองขั้นตอนมีช่องว่างของเวลาที่ทำให้เกิด race condition ได้เสมอเมื่อมี request เข้ามาพร้อมกัน การพึ่งพา constraint ของฐานข้อมูลให้จัดการความถูกต้องแทน เป็นแนวทางที่ทนต่อสถานการณ์แบบนี้ได้ดีกว่าการเขียน logic ตรวจสอบเองทั้งหมดในโค้ด
ทำแบบนี้แล้วพัง เพราะอะไร
ข้อผิดพลาดที่พบบ่อยเมื่อทีมสร้าง webhook receiver ด้วย Supabase Edge Functions ครั้งแรกโดยไม่คุ้นเคยกับ Deno runtime
- ใช้ router ที่ parse body เป็น JSON อัตโนมัติก่อนถึง handler — ทำให้ไม่มีโอกาสเข้าถึง raw body มาคำนวณ signature ได้ถูกต้อง
- ตรวจสอบ identifier ซ้ำก่อน insert แยกสองขั้นตอน — เปิดช่องให้เกิด race condition เมื่อมี retry เข้ามาพร้อมกัน กลายเป็นข้อมูลซ้ำในฐานข้อมูล
- ใช้ secret เดียวกับ service role key สำหรับตรวจ signature — ถ้า secret หลุด ผลกระทบขยายวงกว้างเกินความจำเป็นของแค่การปลอมแปลง webhook
- ลืมตั้ง secret ของ production แยกจาก local dev — ทำให้ Edge Function รันได้ปกติตอนทดสอบในเครื่องแต่ error ทันทีที่ deploy จริง
สรุป
Supabase Edge Functions เหมาะกับ webhook receiver ที่ต้องเขียนข้อมูลลง Postgres ของ project ตัวเองเป็นหลัก เพราะอยู่ใกล้ฐานข้อมูลและเขียนโค้ดได้ในขั้นตอนเดียว แต่ต้องเข้าใจ Deno runtime และใช้ unique constraint ระดับฐานข้อมูลช่วยกัน event ซ้ำแทนการพึ่ง cache ภายนอกอย่างเดียว
การทดสอบ local ด้วย Supabase CLI ช่วยได้มากสำหรับ logic ทั่วไป แต่ยังต้องตรวจสอบพฤติกรรมเรื่อง secret และ cold start บน environment ที่ deploy จริงก่อนเชื่อว่าระบบพร้อมรับ traffic จริงเสมอ
- ใช้ unique constraint ที่ระดับฐานข้อมูลกัน event ซ้ำ แทนการตรวจสอบก่อน insert แยกสองขั้นตอน
- อ่าน raw body มาคำนวณ signature เสมอ อย่าให้ router parse เป็น JSON ก่อนถึง handler
- ตั้ง secret ของ production แยกจาก local dev ทุกครั้งก่อน deploy จริง
คำถามที่พบบ่อย
Supabase Edge Functions ต่อ Postgres ของ project ตัวเองได้โดยตรงไหม
ได้ในระดับหนึ่งเพราะ Deno runtime รองรับ TCP ดีกว่า Cloudflare Workers แต่ยังแนะนำให้ใช้ pooled connection แทน direct connection เพื่อความปลอดภัยด้านจำนวน connection เช่นเดียวกับ pattern ทั่วไป
ทำไมควรใช้ unique constraint แทน cache เก็บสถานะ idempotency
เพราะ unique constraint ที่ระดับฐานข้อมูลป้องกัน race condition ได้ดีกว่าเมื่อมี request หลายตัวเข้ามาพร้อมกัน ขณะที่การตรวจสอบก่อนแล้วค่อยเขียนแยกสองขั้นตอนมีช่องว่างที่ทำให้เกิดข้อมูลซ้ำได้
Deno import module ต่างจาก Node ยังไง
Deno ใช้ URL หรือ npm specifier ตรง ๆ ไม่มี node_modules แบบเดิม รองรับ TypeScript โดยไม่ต้อง compile แยก แต่บาง library ที่พึ่งพา native binding เฉพาะแพลตฟอร์มอาจใช้ไม่ได้ตรง ๆ
ต้องใช้ Supabase CLI ทดสอบ Edge Functions ในเครื่องเสมอไหม
แนะนำให้ใช้เพื่อทดสอบ logic เบื้องต้นก่อน แต่ควรทดสอบพฤติกรรมเรื่อง cold start และ secret ของ production บน environment ที่ deploy จริงด้วย เพราะบางจุดจำลองในเครื่องได้ไม่ครบ
ควรเก็บ event log ไว้ในตารางเดียวกับข้อมูลหลักของระบบไหม
ไม่แนะนำ ควรแยกตาราง event log ออกจากตารางข้อมูลหลัก เพื่อให้ query และดูแล schema ของแต่ละส่วนได้อิสระจากกัน และง่ายต่อการตั้ง retention policy ของ log แยกจากข้อมูลธุรกิจหลัก
Edge Functions กับ Cloudflare Workers เลือกใช้ตัวไหนดีกว่าสำหรับ webhook
ขึ้นกับว่าข้อมูลปลายทางอยู่ที่ไหนเป็นหลัก ถ้าต้องเขียนลง Postgres ของ Supabase เป็นหลักและต้องการความง่ายในการต่อฐานข้อมูล Edge Functions สะดวกกว่า ถ้าต้องการ edge network ที่กว้างกว่าหรือมีปลายทางหลายแบบ Cloudflare Workers อาจเหมาะกว่า
ลองตรวจด้วยตัวเอง
LINE Webhook Payload Inspector
วาง JSON payload จาก LINE แล้วดูว่าเป็น event ชนิดไหน มี field ครบตาม schema ปัจจุบันหรือไม่
ตรวจ payload ฟรี →ใช้ฟรี ไม่ต้องสมัคร ไม่เก็บข้อมูลที่คุณกรอก
อ่านต่อแบบเจาะลึก
วัด Conversion LINE ทุกแบรนด์ รวมศูนย์ที่เดียว
องค์กรที่มีหลายแบรนด์ หลายสาขา หลาย LINE OA — linli รวม Tracking และส่ง Conversion กลับทุกแพลตฟอร์ม พร้อม Custom Limit เริ่มฟรี 14 วัน
ติดต่อทีม Salesบทความที่เกี่ยวข้อง

Vercel Serverless Function กับ Edge Function รับ Webhook ต่างกันตรงไหนเรื่อง Timeout

ทำไม LINE Webhook ที่ตรวจ Signature ถูกแล้วยังโดน Cloudflare Workers ปฏิเสธ
