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

สรุปสั้น ๆ
Session ของ Supabase Auth บน Next.js ที่ deploy บน Vercel มักหลุดเพราะสาเหตุที่ไม่ใช่โค้ด auth ผิด แต่เป็นเรื่อง cookie configuration ที่ไม่ตรงกับ production domain, middleware ที่ไม่ refresh token ก่อนหมดอายุ หรือ preview deployment ที่มี URL เปลี่ยนทุกครั้งจนทำให้ cookie scope ผิดที่ ต้องตรวจทั้งสามจุดนี้ควบคู่กันเพื่อให้ session เสถียรจริงบน production
ทีมพัฒนาที่ตั้งค่า Supabase Auth ตามเอกสารทุกขั้นตอน สร้าง client ถูกต้อง เขียน middleware ตามตัวอย่าง แล้ว test ผ่านหมดในเครื่อง มักคาดว่าเมื่อ deploy ขึ้น Vercel แล้วทุกอย่างจะทำงานเหมือนเดิม แต่ในทางปฏิบัติหลายทีมกลับเจอปัญหาผู้ใช้ถูก log out กะทันหันโดยไม่มีสาเหตุชัดเจน หรือ session ใช้งานได้ปกติตอนแรกแล้วหลุดหลังจากผ่านไปสักพัก
ปัญหานี้มักไม่ได้มาจากโค้ด auth ที่เขียนผิด แต่มาจากรายละเอียดของสภาพแวดล้อม production ที่ต่างจาก local development เช่น cookie ที่ผูกกับ domain เฉพาะ, HTTPS ที่บังคับ secure flag, และ preview deployment ของ Vercel ที่สร้าง URL ใหม่ทุกครั้งจน cookie scope ไม่ตรงกับที่คาดไว้
บทความนี้จะไล่ทีละจุดที่ทำให้ session หลุดบน production ทั้งที่ setup ตามเอกสารทุกอย่างแล้ว พร้อมวิธีตรวจสอบและแก้ไขที่ใช้ได้จริง ไม่ใช่แค่ 'ลองใหม่แล้วจะหาย'
Session ของ Supabase Auth เก็บและ Refresh ยังไงจริง ๆ
Supabase Auth ใช้ JWT สองชุดคือ access token ที่อายุสั้น (มักหลักชั่วโมง) และ refresh token ที่อายุยาวกว่ามาก access token ใช้แนบไปกับทุก request เพื่อยืนยันตัวตน ส่วน refresh token ใช้แลก access token ใหม่เมื่อของเดิมหมดอายุ โดยไม่ต้องให้ผู้ใช้ล็อกอินใหม่
ใน Next.js ที่มีทั้ง Server Component และ Client Component ทำงานร่วมกัน token ทั้งสองต้องถูกเก็บในรูปแบบที่ทั้งฝั่ง server และ client อ่านได้ตรงกัน ซึ่งคือ cookie ไม่ใช่ localStorage ที่ฝั่ง server เข้าถึงไม่ได้ นี่คือเหตุผลที่ต้องใช้ library เฉพาะสำหรับ server-side rendering แทน client library ตรง ๆ
จุดสำคัญคือ cookie ที่เก็บ token ต้องถูก refresh ก่อนที่ access token จะหมดอายุ ถ้าไม่มีกลไก refresh ที่ทำงานถูกจังหวะ ผู้ใช้จะเจออาการ session หลุดแบบสุ่ม ๆ ขึ้นกับว่าพอดี request นั้นมาถึงตอน token หมดอายุพอดีหรือไม่
บทบาทของ Middleware ในการ Refresh Token ก่อนหมดอายุ
รูปแบบมาตรฐานที่แนะนำคือใช้ Next.js middleware ตรวจสอบและ refresh session ทุกครั้งที่มี request เข้ามา ก่อนที่ request นั้นจะไปถึง Server Component หรือ Route Handler middleware ทำหน้าที่อ่าน token จาก cookie เช็คว่าใกล้หมดอายุหรือยัง ถ้าใกล้หมดจะเรียก refresh แล้วเขียน cookie ชุดใหม่กลับไปก่อนส่ง response ต่อ
ถ้าลืมเขียน middleware นี้ หรือเขียนแต่ไม่ได้ครอบคลุมทุก route ที่ต้องการ auth state ผู้ใช้จะเจอ token ที่หมดอายุแล้วแต่ยังไม่ถูก refresh ทำให้ Server Component คิดว่าผู้ใช้ไม่ได้ล็อกอิน ทั้งที่จริงแค่ token เก่าหมดอายุพอดี ปัญหานี้มักถูกเข้าใจผิดว่าเป็นบั๊กเรื่อง auth logic ทั้งที่จริงเป็นเรื่องจังหวะการ refresh
อีกจุดที่ทีมมักพลาดคือตั้งค่า matcher ของ middleware ให้ครอบคลุมไม่ครบทุก path ที่ต้องใช้ auth state เช่นลืมรวม API Route บางเส้นทางไว้ใน matcher ทำให้ route นั้นได้ token เก่าที่ไม่ถูก refresh ในขณะที่ route อื่นทำงานปกติ ทำให้ปัญหาดูเหมือนเกิดแบบสุ่มไม่มีรูปแบบ
Cookie Domain และ Secure Flag ต้องตรงกับ Production จริง
ตอน development บนเครื่อง local ที่รันผ่าน http://localhost cookie มักทำงานได้ง่ายเพราะไม่มีข้อจำกัดเรื่อง secure flag หรือ cross-site มากนัก แต่พอ deploy ขึ้น production ที่รันผ่าน HTTPS จริง cookie ต้องตั้งค่า secure flag ให้ถูกต้อง และถ้าแอปมีการเรียกข้าม subdomain ต้องตั้งค่า domain ของ cookie ให้ครอบคลุมทุก subdomain ที่เกี่ยวข้อง
ปัญหาที่พบบ่อยคือทีมตั้งค่า cookie domain ผูกกับ URL preview deployment ที่ใช้ตอน development แล้วลืมปรับให้ตรงกับ production domain จริงตอน deploy ทำให้ cookie ถูกเขียนแต่ browser ไม่ยอมส่งกลับมาเพราะ domain ไม่ตรง ผลคือผู้ใช้ดูเหมือนล็อกอินสำเร็จตอนแรกแต่ refresh หน้าแล้วหลุดทันที
แนวทางที่ปลอดภัยกว่าคือไม่ hardcode domain ของ cookie ไว้ตรง ๆ ในโค้ด แต่อ่านจาก environment variable ที่ตั้งค่าต่างกันระหว่าง preview และ production บน Vercel เพื่อให้ cookie ทำงานถูกต้องในทุกสภาพแวดล้อมโดยไม่ต้องแก้โค้ดทุกครั้งที่ deploy
Preview Deployment ของ Vercel ทำให้ Cookie Scope ผิดที่ได้ยังไง
จุดเฉพาะของ Vercel ที่ทีมอื่นอาจไม่ได้เจอคือ Preview Deployment สร้าง URL ใหม่ทุกครั้งที่มี pull request ใหม่ (เช่น project-git-branch-hash.vercel.app) ถ้า Supabase Auth ถูกตั้งค่า redirect URL หรือ allowed origin ไว้แค่ URL production เดียว การล็อกอินบน preview deployment อาจ redirect กลับมาผิดที่ หรือถูกปฏิเสธเพราะ origin ไม่ตรงกับที่อนุญาตไว้
ทางแก้คือตั้งค่า allowed redirect URL ใน Supabase Auth settings ให้รองรับ pattern ของ preview deployment ด้วย ไม่ใช่แค่ production URL เพียงอย่างเดียว บาง Supabase project รองรับ wildcard pattern สำหรับ preview URL ซึ่งควรตรวจ documentation ล่าสุดว่ารองรับ pattern แบบไหนก่อนตั้งค่า
อีกวิธีที่หลายทีมใช้คือแยก Supabase project สำหรับ preview/staging ออกจาก production ไปเลย ตามที่กล่าวถึงในบทความ วาง Next.js Supabase Vercel ให้รันโปรดักชันจริง ซึ่งช่วยแก้ปัญหา redirect URL ไปพร้อมกับการแยกข้อมูลทดสอบออกจากข้อมูลจริงด้วย
ตัวอย่างสมมติ: ไล่ Debug Session ที่หลุดเฉพาะบางกลุ่มผู้ใช้
ลองดูเคสตัวอย่างสมมติของทีมหนึ่งที่ได้รับแจ้งจากผู้ใช้ว่า login แล้วหลุดบ่อยผิดปกติ แต่ทีม dev เองทดสอบแล้วไม่เจอปัญหา ทำให้ debug ยากในช่วงแรก (รายละเอียดทั้งหมดเป็นตัวอย่างสมมติเพื่อประกอบการอธิบายขั้นตอนไล่ปัญหา)
หลังเก็บข้อมูลเพิ่มเติมพบว่าผู้ใช้ที่ร้องเรียนส่วนใหญ่เป็นกลุ่มที่เปิดแอปทิ้งไว้นานเกิน 1 ชั่วโมงโดยไม่ได้ interact อะไร ขณะที่ทีม dev เองมักทดสอบแบบ refresh หน้าใหม่บ่อย ๆ ซึ่งทำให้ token ถูก refresh อยู่ตลอดโดยไม่รู้ตัว จุดนี้ทำให้สงสัยว่าปัญหาอยู่ที่กลไก refresh ตอน token ใกล้หมดอายุพอดี
การตรวจ middleware พบว่า matcher ที่ตั้งไว้ไม่ครอบคลุม API Route บางเส้นทางที่ frontend เรียกแบบ background (เช่น polling ข้อมูลแจ้งเตือน) ทำให้ token ของ route เหล่านั้นไม่เคยถูก refresh เมื่อ access token หมดอายุ request ที่เรียกผ่าน route นี้จะได้ error unauthorized ทำให้แอปแสดงผลว่าผู้ใช้หลุด login ทั้งที่ route อื่นยังทำงานปกติ
ทีมแก้ปัญหาด้วยการปรับ matcher ของ middleware ให้ครอบคลุมทุก path ที่ต้องใช้ auth state รวมถึง API Route แบบ background ทั้งหมด หลังแก้ไขจำนวนรายงานปัญหา session หลุดลดลงอย่างชัดเจนในสัปดาห์ถัดมา บทเรียนสำคัญคือปัญหาแบบนี้มักไม่โผล่ตอนทดสอบแบบปกติ เพราะพฤติกรรมการใช้งานของนักพัฒนากับผู้ใช้จริงต่างกัน
Server Action ต้องเช็ค Auth เองซ้ำ ไม่พึ่ง Middleware อย่างเดียว
จุดหนึ่งที่ทีมมักเข้าใจผิดคือคิดว่า middleware ที่ป้องกัน route ไว้แล้วเพียงพอสำหรับทุกการกระทำในแอป แต่ Server Action ของ Next.js สามารถถูกเรียกได้จากหลายทางแม้ผู้ใช้จะไม่ได้อยู่ที่หน้าที่ middleware ป้องกันไว้โดยตรง เช่นถูกเรียกผ่าน form ที่อาจถูก submit ซ้ำหลังจาก session หมดอายุไปแล้วขณะเปิดหน้าค้างไว้
แนวทางที่ปลอดภัยกว่าคือให้ทุก Server Action ที่แก้ไขข้อมูลสำคัญตรวจสอบ session ของตัวเองอีกชั้นก่อนทำงานจริง ไม่ใช่เชื่อว่า middleware กรองมาให้แล้วทั้งหมด เพราะ middleware ทำงานตอน request เข้ามาถึง route เท่านั้น แต่ Server Action อาจถูกเรียกในจังหวะที่ session เปลี่ยนสถานะไปแล้วระหว่างที่ผู้ใช้เปิดหน้าค้างไว้นาน
การตรวจสอบซ้ำนี้ไม่ได้ทำให้ระบบช้าลงอย่างมีนัยสำคัญ เพราะเป็นการอ่านค่าจาก cookie ที่มีอยู่แล้ว ไม่ต้อง query database เพิ่มเติมในกรณีส่วนใหญ่ แต่ช่วยป้องกันเคสที่ session หมดอายุกลางทางแล้ว action ยังหลุดไปทำงานโดยไม่มีการตรวจสอบใด ๆ
Race Condition ตอน Refresh Token พร้อมกันหลาย Request
อาการ session หลุดแบบสุ่มโดยไม่มีรูปแบบชัดเจนบางครั้งมาจาก race condition ระหว่างการ refresh token เมื่อผู้ใช้เปิดหลาย tab พร้อมกัน หรือแอปยิง request หลายตัวพร้อมกันในจังหวะที่ token ใกล้หมดอายุพอดี แต่ละ request อาจพยายาม refresh token พร้อมกัน ทำให้เกิด token คนละชุดที่แข่งกันเขียนทับ cookie
ผลลัพธ์ที่เกิดขึ้นได้คือ request หนึ่งได้ token ใหม่ที่ถูกต้อง แต่อีก request หนึ่งได้ token ที่ถูกเขียนทับไปแล้วจนใช้งานไม่ได้ ทำให้ผู้ใช้เห็นพฤติกรรมไม่สม่ำเสมอ เช่นบาง tab ใช้งานได้ปกติ แต่อีก tab ต้อง login ใหม่ทั้งที่เป็นผู้ใช้คนเดียวกัน
การลดปัญหานี้ทำได้หลายทาง เช่นใช้กลไก lock หรือ debounce ฝั่ง client ไม่ให้ยิง refresh พร้อมกันหลาย request ในเวลาเดียวกัน หรือให้ middleware ฝั่ง server เป็นจุดเดียวที่ตัดสินใจ refresh แทนที่จะให้ client library แต่ละ tab ตัดสินใจแยกกัน ทีมที่เจออาการนี้บ่อยควรตรวจ pattern การเรียก auth client ว่ามีจุดไหนที่เรียกซ้ำซ้อนโดยไม่จำเป็นหรือไม่ ก่อนจะสรุปว่าเป็นบั๊กของ library เอง
ตารางไล่อาการ Session หลุด กับสาเหตุที่เป็นไปได้
เพื่อให้ทีมอื่นไล่ปัญหาได้เร็วขึ้น นี่คือตารางสรุปอาการที่พบบ่อยกับจุดที่ควรตรวจก่อน
| อาการที่เจอ | จุดที่ควรตรวจก่อน |
|---|---|
| ล็อกอินสำเร็จแต่ refresh หน้าแล้วหลุดทันที | cookie domain/secure flag ไม่ตรงกับ production URL |
| หลุดเฉพาะบน preview deployment ไม่หลุดบน production | allowed redirect URL ใน Supabase Auth ไม่รองรับ preview URL pattern |
| หลุดเฉพาะผู้ใช้ที่เปิดแอปทิ้งไว้นาน | middleware ไม่ refresh token ก่อนหมดอายุ หรือ matcher ไม่ครอบคลุมบาง route |
| หลุดแบบสุ่ม ไม่มีรูปแบบชัดเจน | มี race condition ระหว่าง request พร้อมกันหลายตัวที่พยายาม refresh token เวลาเดียวกัน |
ขั้นตอนป้องกันก่อนปล่อย Auth Flow เข้า Production
เพื่อลดโอกาสเจอปัญหาแบบข้างต้น ควรทำตามลำดับขั้นตอนนี้ก่อนปล่อย auth flow เข้า production จริง
- ตรวจ matcher ของ middleware ให้ครอบคลุมทุก route ที่ต้องใช้ auth state รวมถึง API Route แบบ background
- ตั้งค่า cookie domain ผ่าน environment variable แยกตามสภาพแวดล้อม ไม่ hardcode ค่าตายตัวในโค้ด
- เพิ่ม allowed redirect URL สำหรับ preview deployment pattern ใน Supabase Auth settings ไม่ใช่แค่ production URL
- ทดสอบ flow login แล้วปล่อยทิ้งไว้เกินอายุของ access token เพื่อยืนยันว่า refresh ทำงานจริงโดยไม่ต้อง login ใหม่
- ทดสอบบน preview deployment จริงอย่างน้อยหนึ่งรอบก่อน merge เข้า production ไม่ใช่ทดสอบแค่บนเครื่อง local
สรุป
ปัญหา session หลุดบน production ที่ทำตามเอกสารครบทุกขั้นตอนแล้วยังเจอ มักไม่ได้มาจากโค้ด auth logic ผิด แต่มาจากรายละเอียดของสภาพแวดล้อม production ที่ต่างจาก local development เช่น cookie domain, secure flag และ URL ของ preview deployment ที่เปลี่ยนทุกครั้ง
การป้องกันที่ดีที่สุดคือทดสอบ auth flow บนสภาพแวดล้อมที่ใกล้เคียง production จริงตั้งแต่ช่วง review code ไม่ใช่รอให้ผู้ใช้จริงมาเจอปัญหาก่อนแล้วค่อยไล่ debug ย้อนหลัง
- ตรวจ matcher ของ middleware ให้ครอบคลุมทุก route ที่ใช้ auth state รวมถึง background API
- ตั้งค่า cookie domain และ secure flag ผ่าน environment variable แยกตามสภาพแวดล้อม
- เพิ่ม allowed redirect URL สำหรับ preview deployment pattern ใน Supabase Auth settings
คำถามที่พบบ่อย
ทำไม session ทำงานปกติตอน development แต่หลุดตอน deploy จริง
เพราะ local development มักรันผ่าน http://localhost ที่ไม่มีข้อจำกัดเรื่อง secure flag หรือ domain มากเท่า production ที่รันผ่าน HTTPS จริง ควรทดสอบบน preview deployment ที่ใกล้เคียง production ก่อน merge เสมอ
ต้องตั้งค่า cookie เองทั้งหมดไหม หรือ Supabase library จัดการให้
library ฝั่ง server-side rendering ของ Supabase จัดการ cookie ให้ในระดับหนึ่ง แต่ทีมยังต้องตั้งค่า domain, secure flag และ matcher ของ middleware ให้ตรงกับ production environment เอง เพราะเป็นค่าที่ผูกกับโปรเจกต์แต่ละอัน
Preview Deployment ของ Vercel ต้องใช้ Supabase project แยกไหม
แนะนำให้แยก เพื่อความปลอดภัยของข้อมูล production และเพื่อลดความซับซ้อนของการตั้งค่า allowed redirect URL ที่ต้องรองรับ URL ที่เปลี่ยนทุกครั้งของ preview deployment
ทำไม session หลุดเฉพาะบาง route ไม่หลุดทั้งแอป
มักเกิดจาก matcher ของ middleware ไม่ครอบคลุมทุก route ที่ต้องใช้ auth state ทำให้บาง route ได้ token เก่าที่ไม่ถูก refresh ขณะที่ route อื่นทำงานปกติ
จำเป็นต้อง log ทุกครั้งที่ token refresh ไหม
ควรมี log อย่างน้อยในระดับที่ตรวจสอบย้อนหลังได้ว่า refresh สำเร็จหรือ fail เพราะช่วยไล่ปัญหาได้เร็วกว่ามารอผู้ใช้ร้องเรียนแล้วค่อยเริ่มสืบ
มีวิธีทดสอบ session refresh โดยไม่ต้องรอ token หมดอายุจริงไหม
บางทีมปรับอายุ token ให้สั้นลงชั่วคราวเฉพาะใน environment ทดสอบ เพื่อให้เห็นพฤติกรรม refresh เร็วขึ้นโดยไม่ต้องรอเป็นชั่วโมง แล้วค่อยปรับกลับเป็นค่าปกติก่อนใช้งานจริง
อ่านต่อแบบเจาะลึก
ทดลองวัดผลโฆษณาเข้า LINE ฟรี 14 วัน
สมัครแล้วสร้างโปรเจกต์ เชื่อม LINE ติดตั้ง Tracking แล้วเริ่มเห็น Funnel จาก Click ถึง Purchase ได้ทันที ไม่ต้องใช้บัตรเครดิต
เริ่มทดลองใช้ฟรี 14 วันบทความที่เกี่ยวข้อง

ตั้ง Webhook LINE บน Vercel แล้ว Event หายไปครึ่งหนึ่ง ต้องแก้ตรงไหนก่อน

ผู้ใช้ทักเข้ามาวันละหลักพันข้อความ แต่ Bot เริ่มค้างและตอบช้าเป็นบางช่วง เกิดจากอะไร
