ทีมเล็กวาง Next.js Supabase Vercel ให้รันโปรดักชันจริงโดยไม่ต้องคอยตื่นมาแก้กลางดึก

สรุปสั้น ๆ
Next.js บน Vercel ต่อกับ Supabase ทำงานร่วมกันได้ดีในระดับ prototype แทบไม่ต้องคิดอะไรเพิ่ม แต่พอเข้า production จริงต้องจัดการ connection pooling ระหว่าง serverless function กับ Postgres, แยก environment variable ให้ถูกสภาพแวดล้อม และวางลำดับ migration กับ deploy ให้สอดคล้องกัน ไม่งั้นจะเจอปัญหา connection พุ่งหรือ session หลุดตอน traffic เพิ่มกะทันหัน
สาม-สี่ปีที่ผ่านมา Next.js คู่กับ Supabase แล้ว deploy บน Vercel กลายเป็น stack ที่ทีมพัฒนาขนาดเล็กถึงกลางเลือกใช้กันเยอะมาก เพราะทั้งสามฝั่งต่อกันได้ง่าย มี SDK ให้ใช้ตรง ๆ ไม่ต้องเขียน infrastructure เอง และ pricing ช่วงเริ่มต้นก็เข้าถึงได้ ทีมที่มีนักพัฒนาสองสามคนสามารถมี production stack ที่ดูเป็นมืออาชีพได้ภายในไม่กี่วัน
แต่มีช่องว่างหนึ่งที่เอกสารเริ่มต้นมักไม่ค่อยพูดถึงชัด ๆ คือพอ traffic เริ่มมากขึ้นจริง หรือมี feature ที่ต้องเรียก database บ่อยขึ้น ปัญหาที่โผล่มาไม่ใช่เรื่อง framework ผิดหรือถูก แต่เป็นเรื่องการจัดการ connection ระหว่าง serverless function ของ Vercel กับ Postgres ของ Supabase ที่มีจำนวน connection จำกัด และเรื่อง environment ที่แยกไม่ชัดระหว่าง preview deployment กับ production
บทความนี้ไม่ได้จะบอกว่า stack นี้มีปัญหาอะไรร้ายแรง เพราะทีมจำนวนมากใช้งานได้จริงในระดับ production มาหลายปีแล้ว แต่จะเจาะจุดที่ทีมเล็กมักตกหลุมตอนเริ่มมี traffic จริง พร้อมวิธีวางโครงสร้างตั้งแต่ต้นเพื่อไม่ต้องมาแก้ทีหลังตอนระบบพังกลางดึก
ทำไม Next.js Supabase Vercel ถึงมาคู่กันบ่อยในทีมเล็ก
บทบาทของแต่ละตัวในสถาปัตยกรรมนี้ค่อนข้างชัดเจน Vercel รับหน้าที่ build และ deploy แอป Next.js พร้อมรัน API Route หรือ Server Action เป็น serverless function กระจายตาม region ที่ตั้งค่าไว้ Supabase รับหน้าที่เป็น Postgres database พร้อมชั้น Auth, Storage และ Realtime ที่ผูกกับตัวฐานข้อมูลเดียวกัน ส่วน Next.js เองเป็นเฟรมเวิร์กที่เชื่อมสองฝั่งนี้เข้าด้วยกันผ่าน Server Component, Route Handler และ Client SDK
จุดที่ทำให้ทีมเล็กเลือก stack นี้คือไม่ต้องตั้ง server เอง ไม่ต้องดูแล database ตัวเอง และ deploy ใหม่ทุกครั้งที่ push code ขึ้น git โดยอัตโนมัติผ่าน Vercel ทำให้ทีมโฟกัสกับ feature ได้มากกว่าโครงสร้างพื้นฐาน แต่การไม่ต้องดูแล infrastructure เองไม่ได้แปลว่าไม่ต้องเข้าใจว่ามันทำงานยังไงข้างใน โดยเฉพาะเรื่อง connection ระหว่าง serverless function กับ database
สิ่งที่ต้องเข้าใจตั้งแต่ต้นคือ Vercel function แต่ละ instance เป็น process แยกกัน เวลามี traffic เข้ามาพร้อมกันหลาย request Vercel อาจสร้าง instance ใหม่ขึ้นมาหลายตัวพร้อมกัน แต่ละ instance ถ้าเปิด connection ใหม่ไปหา Postgres ทุกครั้งโดยไม่มีการจัดการที่ดี จะทำให้จำนวน connection พุ่งเร็วกว่าที่ Postgres รองรับไหว
จัดการ Connection Pooling ระหว่าง Vercel Function กับ Postgres
Postgres ทั่วไปรองรับจำนวน connection พร้อมกันได้จำกัด ขึ้นกับขนาดของ instance ที่ Supabase จัดสรรให้ตามแพ็กเกจ ขณะที่ serverless function บน Vercel สามารถ scale ขึ้นเป็นหลักสิบหรือหลักร้อย instance พร้อมกันได้ในช่วง traffic สูง ถ้าแต่ละ instance เปิด direct connection ไปหา Postgres ตรง ๆ จำนวน connection จะชนเพดานได้เร็วกว่าที่คิด แล้ว request ใหม่ที่เข้ามาจะได้ error ประเภท connection ถูกปฏิเสธ
ทางแก้มาตรฐานคือใช้ connection pooler ที่ Supabase มีให้อยู่แล้ว ซึ่งทำหน้าที่รับ connection จำนวนมากจากฝั่ง client แล้วจัดสรรไปใช้ connection จริงกับ Postgres จำนวนจำกัดที่บริหารจัดการอย่างมีประสิทธิภาพ การเลือกใช้ connection string แบบ pooled (มักจะเป็นพอร์ตที่ต่างจาก direct connection) แทนการต่อ Postgres ตรง ๆ ช่วยลดปัญหานี้ได้มากในสภาพแวดล้อม serverless
อีกจุดที่มักถูกมองข้ามคือโหมดของ pooler เอง บาง mode เหมาะกับ query สั้น ๆ ที่ไม่ค้าง transaction นาน บาง mode เหมาะกับงานที่ต้องการ session state ต่อเนื่อง เช่นการใช้ prepared statement หรือ transaction หลายคำสั่งต่อกัน ทีมที่เลือก mode ผิดประเภทอาจเจอ error แปลก ๆ ที่ debug ยากเพราะดูเหมือน query ถูกต้องทุกอย่างแต่ปฏิเสธการทำงานบางแบบ ควรตรวจ documentation ล่าสุดของ Supabase ก่อนเลือก mode ให้ตรงกับ workload จริงของแอป
สำหรับทีมที่ใช้ ORM อย่าง Prisma หรือ Drizzle ก็ต้องตั้งค่า connection string ให้ตรงกับ pooler ด้วยเช่นกัน บาง ORM ต้องการ direct connection แยกต่างหากสำหรับตอนรัน migration เพราะ migration บางคำสั่งต้องการ session ที่ต่อเนื่องกว่าที่ pooler รองรับ การแยก connection string สองชุดตั้งแต่ต้น (ชุดสำหรับรัน query ปกติผ่าน pooler และชุดสำหรับรัน migration แบบ direct) ช่วยลดความสับสนตอน deploy ได้มาก
แยก Environment Variables ให้ถูกสภาพแวดล้อม ไม่ปนกันระหว่าง Preview กับ Production
Vercel มีจุดเด่นเรื่อง Preview Deployment ที่สร้าง URL แยกให้ทุก pull request โดยอัตโนมัติ ซึ่งมีประโยชน์มากตอน review code แต่ก็เป็นจุดเสี่ยงถ้าทีมไม่ได้แยก environment variable ให้ชัดเจน เพราะ preview deployment ที่ต่อกับ Supabase project เดียวกับ production อาจทำให้ข้อมูลทดสอบไปปนกับข้อมูลจริงโดยไม่ตั้งใจ
แนวทางที่ปลอดภัยกว่าคือมี Supabase project แยกสำหรับ production กับ development/staging อย่างน้อยสองชุด แล้วตั้งค่า Environment Variable บน Vercel ให้แยกตาม environment (Production, Preview, Development) ให้ preview deployment ทุกอันชี้ไปที่ project staging เท่านั้น ไม่มีทางแตะ production database ได้แม้จะเป็นแค่ branch ทดสอบ
อีกจุดที่ต้องระวังคือ service role key ของ Supabase ซึ่งมีสิทธิ์ข้าม Row Level Security ทุกกฎ ต้องตั้งเป็น environment variable ฝั่งเซิร์ฟเวอร์เท่านั้น (ไม่มี prefix NEXT_PUBLIC_) และใช้เฉพาะใน Server Component, Route Handler หรือ Server Action ที่ไม่ถูกส่งไปฝั่ง client อ่านรายละเอียดเรื่อง Row Level Security เพิ่มเติมได้ในบทความ Supabase RLS คืออะไร เพราะการเข้าใจ RLS ให้ถูกต้องช่วยลดความจำเป็นต้องใช้ service role key พร่ำเพรื่อ
Session ของ Supabase Auth ทำงานยังไงใน Next.js App Router
Supabase Auth เก็บ session เป็น token คู่หนึ่งคือ access token กับ refresh token โดย client library จะจัดการ refresh ให้อัตโนมัติเมื่อ token ใกล้หมดอายุ แต่ใน Next.js App Router ที่มีทั้ง Server Component และ Client Component ทำงานร่วมกัน ต้องมีการ sync session ระหว่างสองฝั่งผ่าน cookie ไม่ใช่แค่เก็บใน localStorage แบบแอปฝั่ง client ล้วน ๆ
รูปแบบที่แนะนำคือใช้ middleware ของ Next.js ตรวจสอบและ refresh session ทุกครั้งที่มี request เข้ามา ก่อนที่ Server Component จะอ่านค่า session จาก cookie เพื่อ render หน้าที่ต้องใช้ auth state ให้ถูกต้องตั้งแต่ครั้งแรกที่โหลดหน้า ไม่ต้องรอ client-side hydrate ก่อนถึงจะรู้ว่าผู้ใช้ล็อกอินอยู่หรือไม่
ปัญหาที่พบบ่อยเมื่อ setup ผิดคือ session ที่ดูเหมือนใช้ได้ปกติตอน development แต่หลุดบ่อยตอน deploy จริงบน Vercel เพราะ cookie ไม่ถูกตั้งค่า domain หรือ secure flag ให้ตรงกับ production URL หรือเพราะ middleware ไม่ได้ refresh token ก่อนหมดอายุจริง ควรทดสอบ flow การ login ค้างไว้นาน ๆ บน preview deployment ก่อน merge เข้า production เสมอ
วางลำดับ Deploy กับ Database Migration ให้ไม่ชนกัน
ปัญหาคลาสสิกของ stack นี้คือ deploy แอปใหม่กับรัน migration ฐานข้อมูลเป็นคนละขั้นตอนที่แยกจากกัน ถ้า migration เปลี่ยน schema แบบ breaking change เช่นเปลี่ยนชื่อ column แต่ deploy แอปใหม่ที่ใช้ schema ใหม่ยังไม่เสร็จ ช่วงเวลาสั้น ๆ ระหว่างนั้นอาจมี request จาก instance เก่าที่ยังอ้าง column ชื่อเดิมอยู่ ทำให้ error กระจายไปยังผู้ใช้จริง
แนวทางที่ปลอดภัยกว่าคือทำ migration แบบ backward compatible เสมอเท่าที่ทำได้ เช่นถ้าจะเปลี่ยนชื่อ column ให้เพิ่ม column ใหม่ก่อน เขียนโค้ดให้รองรับทั้งสองชื่อชั่วคราว รอให้ deploy เสร็จสมบูรณ์ทุก instance แล้วค่อยลบ column เก่าใน migration รอบถัดไป วิธีนี้ทำให้ไม่มีช่วงเวลาที่ schema กับโค้ดไม่ตรงกัน
สำหรับทีมที่ใช้ GitHub Actions ควบคุมการรัน migration ก่อน trigger deploy บน Vercel สามารถอ่านแนวทางตั้ง pipeline เพิ่มเติมได้ในบทความ ต่อ GitHub Actions กับ Supabase ซึ่งช่วยให้ migration รันเป็นขั้นตอนที่ตรวจสอบได้ ไม่ใช่รันมือทุกครั้งที่ deploy
ตัวอย่างสมมติ: ทีม SaaS 3 คนที่ Traffic โตเร็วกว่าที่วางแผนไว้
เพื่อให้เห็นภาพชัดขึ้น ลองดูตัวเลขตัวอย่างสมมติของทีม SaaS ขนาดเล็กทีมหนึ่ง ที่ตอนเริ่มมีผู้ใช้ราว 200 คนต่อวัน เปิด direct connection จาก Vercel function ไปหา Postgres ตรง ๆ โดยไม่ใช้ pooler เพราะตอนเทสต์ในเครื่องไม่เจอปัญหาอะไร
พอมี campaign การตลาดทำให้ traffic พุ่งขึ้นเป็นราว 3,000 ผู้ใช้ในวันเดียว จำนวน concurrent request ที่เข้ามาพร้อมกันในช่วงพีคทำให้ Vercel สร้าง function instance ขึ้นมาพร้อมกันหลายสิบตัว แต่ละตัวพยายามเปิด connection ใหม่ไปหา Postgres จนจำนวน connection รวมเกินเพดานของแพ็กเกจที่ใช้อยู่ ผู้ใช้บางส่วนเจอหน้า error ตอนพยายามล็อกอินหรือบันทึกข้อมูล
ทีมแก้ปัญหาด้วยการเปลี่ยน connection string ให้ผ่าน pooled connection ของ Supabase แทน direct connection ทั้งหมด พร้อมตั้งค่า connection limit ต่อ function ให้เหมาะสม ผลคือจำนวน connection ที่ Postgres เห็นจริงลดลงเหลือหลักสิบแทนที่จะเป็นหลักร้อยแม้ traffic จะเท่าเดิม เพราะ pooler ทำหน้าที่ใช้ connection จริงร่วมกันแทนที่จะให้แต่ละ instance ครอบครอง connection ของตัวเอง (ตัวเลขทั้งหมดเป็นตัวอย่างสมมติเพื่อประกอบการอธิบาย ไม่ใช่ benchmark จริงจากระบบใดระบบหนึ่ง)
บทเรียนจากเคสสมมตินี้คือปัญหาเรื่อง connection มักไม่โผล่ตอน development เพราะ traffic น้อยและ instance ไม่ scale ขึ้นพร้อมกันหลายตัว แต่จะโผล่ทันทีที่มี traffic จริงในระดับที่ทำให้ serverless platform ต้อง scale ขึ้นแบบขนาน การตั้งค่า pooler ให้ถูกตั้งแต่ต้นจึงคุ้มค่ากว่าการมาแก้ตอนระบบพังจริง
ตารางเทียบวิธีต่อ Database จาก Vercel Function แบบต่าง ๆ
เพื่อสรุปให้เห็นภาพรวม นี่คือตารางเทียบวิธีต่อ Postgres ของ Supabase จากฝั่ง Vercel function ที่พบบ่อยที่สุด
| วิธีต่อ Database | เหมาะกับงานแบบไหน | ความเสี่ยงหลัก |
|---|---|---|
| Direct Connection | งาน migration หรือ script ที่รันครั้งเดียว ไม่ผ่าน traffic จริง | connection พุ่งเร็วถ้าใช้กับ serverless function ปกติ |
| Pooled Connection (Transaction Mode) | query สั้น ๆ ต่อ request ปกติของแอป | ไม่รองรับ session state บางแบบ เช่น prepared statement ค้าง |
| Pooled Connection (Session Mode) | งานที่ต้องการ session ต่อเนื่องภายใน request เดียว | ใช้ connection ต่อ request นานกว่า transaction mode เล็กน้อย |
| Edge Function ของ Supabase เอง | งานที่ต้องอยู่ใกล้ database มาก หรือซ่อน secret key เฉพาะ | ต้อง deploy แยกจากแอปหลัก เพิ่มจุดดูแลอีกหนึ่งจุด |
ขั้นตอนตรวจก่อนปล่อย Production จริง
ก่อนปล่อยแอปที่ใช้ stack นี้เข้า production จริง มีลำดับขั้นตอนที่ควรตรวจให้ครบ เพื่อลดโอกาสเจอปัญหาแบบตัวอย่างสมมติข้างต้น
- ยืนยันว่า connection string ที่ใช้ในโค้ด production เป็นแบบ pooled ไม่ใช่ direct connection ที่หลงเหลือจากตอน development
- แยก Supabase project ระหว่าง production กับ staging/preview ให้ไม่ใช้ database ก้อนเดียวกัน
- ตั้งค่า Environment Variable บน Vercel ให้แยกตาม environment ครบทุกตัวแปร ไม่มีตัวไหนหลุดไปใช้ค่า default
- ทดสอบ flow login แล้วปล่อยค้างไว้เกินเวลาหมดอายุของ token เพื่อดูว่า middleware refresh session ได้จริงหรือไม่
- ทดสอบยิง request จำนวนมากพร้อมกัน (load test เบื้องต้น) เพื่อดูว่าจำนวน connection ที่ Postgres เห็นยังอยู่ในระดับปลอดภัยหรือไม่
- เตรียมแผน migration แบบ backward compatible สำหรับการเปลี่ยน schema ทุกครั้งที่มีผลกระทบต่อ production data
ทำแบบนี้แล้วพัง เพราะอะไร
จากการใช้งานจริงของหลายทีม มีรูปแบบความผิดพลาดที่เกิดซ้ำ ๆ เมื่อวาง stack นี้แบบเร่งรีบโดยไม่ทบทวนโครงสร้าง
- ใช้ direct connection ทุก environment — เพราะตอน development ไม่เห็นปัญหา แต่พอ traffic จริงเข้ามาพร้อมกันหลาย request จำนวน connection พุ่งเกินเพดานทันที
- ใช้ Supabase project เดียวกันทั้ง preview และ production — ทำให้ pull request ที่ยังทดสอบอยู่มีสิทธิ์เขียนทับข้อมูลจริงได้โดยไม่ตั้งใจ
- ฝัง service role key ไว้ในโค้ดฝั่ง client — ทำให้ทุกคนที่เปิด dev tools เห็น key ที่ข้าม RLS ได้ทั้งระบบ
- รัน migration แบบ breaking change โดยไม่รอ deploy เสร็จทุก instance — เกิดช่วงเวลาสั้น ๆ ที่ schema กับโค้ดไม่ตรงกัน ทำให้ error กระจายไปหาผู้ใช้จริง
สรุป
Next.js Supabase Vercel เป็น stack ที่ใช้งานได้จริงในระดับ production ไม่ใช่แค่เหมาะกับ prototype แต่ต้องเข้าใจว่า serverless function กับ database มีข้อจำกัดเรื่อง connection ที่ต้องจัดการตั้งแต่ต้น ไม่ใช่รอให้ปัญหาโผล่ตอน traffic สูงแล้วค่อยแก้
จุดที่คุ้มค่าที่สุดในการลงทุนเวลาตั้งแต่ต้นคือการใช้ pooled connection ให้ถูก mode แยก environment ให้ชัดเจน และวางแผน migration แบบ backward compatible เพราะสามจุดนี้เป็นสาเหตุของปัญหาที่พบบ่อยที่สุดเมื่อทีมเล็กเริ่มมี traffic จริง
- ใช้ pooled connection แทน direct connection สำหรับ traffic ปกติของแอป
- แยก Supabase project ระหว่าง production กับ staging/preview อย่างเด็ดขาด
- ทดสอบ session refresh และ load test เบื้องต้นก่อนปล่อย production จริงทุกครั้ง
คำถามที่พบบ่อย
จำเป็นต้องใช้ Supabase Edge Functions ร่วมกับ Vercel เสมอไปไหม
ไม่จำเป็น หลายแอปใช้แค่ Next.js API Route หรือ Server Action บน Vercel ก็เพียงพอแล้วสำหรับ logic ฝั่งเซิร์ฟเวอร์ส่วนใหญ่ Edge Functions ของ Supabase เหมาะเมื่อต้องอยู่ใกล้ database เป็นพิเศษหรือต้องแยก deploy ออกจากแอปหลัก
ควรใช้ Prisma หรือ Supabase client library โดยตรงดีกว่ากัน
ขึ้นกับความคุ้นเคยของทีมและความต้องการ type-safety ทั้งสองทางต่อ Postgres ของ Supabase ได้เหมือนกัน แต่ต้องตั้งค่า connection string ให้ผ่าน pooler ให้ถูกต้องไม่ว่าจะเลือกทางไหน
ทำไม connection error ถึงเกิดเฉพาะตอน traffic สูงเท่านั้น
เพราะ serverless function จะ scale ขึ้นเป็นหลาย instance พร้อมกันเฉพาะตอน traffic สูง แต่ละ instance ถ้าเปิด direct connection เองจะรวมกันเกินเพดานของ Postgres ได้เร็ว การใช้ pooled connection ช่วยลดปัญหานี้ได้มาก
Preview Deployment ของ Vercel ปลอดภัยพอจะต่อ production database ไหม
ไม่แนะนำ ควรแยก Supabase project สำหรับ staging ต่างหาก แล้วให้ preview deployment ทุกอันชี้ไปที่ staging เท่านั้น เพื่อไม่ให้โค้ดที่ยังไม่ผ่าน review มีสิทธิ์เขียนข้อมูลจริง
ถ้า session หลุดบ่อยบน production ควรเริ่มตรวจจุดไหนก่อน
เริ่มตรวจ middleware ว่า refresh token ก่อนหมดอายุจริงหรือไม่ ตามด้วยการตั้งค่า cookie เรื่อง domain และ secure flag ให้ตรงกับ production URL เพราะสองจุดนี้เป็นสาเหตุที่พบบ่อยที่สุด
ต้องรัน load test ก่อน launch จริงทุกครั้งไหม
ควรทำอย่างน้อยแบบเบื้องต้นสำหรับแอปที่คาดว่าจะมี traffic เข้าพร้อมกันจำนวนมาก เช่นช่วง campaign การตลาด เพื่อดูว่าจำนวน connection ที่ Postgres เห็นยังอยู่ในระดับปลอดภัยก่อนเจอ traffic จริง
อ่านต่อแบบเจาะลึก
ทดลองวัดผลโฆษณาเข้า LINE ฟรี 14 วัน
สมัครแล้วสร้างโปรเจกต์ เชื่อม LINE ติดตั้ง Tracking แล้วเริ่มเห็น Funnel จาก Click ถึง Purchase ได้ทันที ไม่ต้องใช้บัตรเครดิต
เริ่มทดลองใช้ฟรี 14 วันบทความที่เกี่ยวข้อง

มี Guardrails แล้วทำไม AI Agent ของเรายังหลุดตอบเรื่องที่ไม่ควรตอบ

ตั้งค่า Supabase Auth ถูกทุกขั้นตอนตามคู่มือ แต่ทำไม Session บน Vercel ยังหลุดกลางทาง
