บทความ
เรื่องราว ความรู้ และประสบการณ์จากการสร้างเว็บไซต์และ Web Application
Supabase Auth Next.js ทำระบบล็อกอิน 2026 | KittiStackBlogBackend & DatabaseSupabase Auth Next.js ทำระบบล็อกอิน 2026
ตั้ง Supabase Auth ใน Next.js App Router แบบปี 2026 ด้วย @supabase/ssr, proxy.ts และ getClaims ล็อกอินด้วยอีเมล Google หรือ LINE ผ่าน Custom OIDC พร้อมตั้ง RLS
Kittipong C.
ทีมงาน KittiStack
October 26, 2026·8 min read

Supabase Auth Next.js ทำยังไง ระบบล็อกอินแบบ App Router ที่ปลอดภัย (อัปเดต 2026)
Supabase Auth Next.js ใช้แพ็กเกจ @supabase/ssr ให้ session เก็บใน cookie มีไฟล์หลัก 3 ไฟล์ คือ client สำหรับเบราว์เซอร์ client สำหรับฝั่ง server และไฟล์ proxy.ts ที่ช่วยต่ออายุ token ทุก request พอตั้งเสร็จ จะล็อกอินด้วยอีเมลกับรหัสผ่าน Google หรือ LINE ก็ได้ และเช็กผู้ใช้ใน Server Component ได้ทันที
ช่วงปี 2025–2026 มีเรื่องที่เปลี่ยนจนบทความเก่าหลายแห่งใช้ไม่ได้แล้ว Next.js 16 เปลี่ยนชื่อ middleware.ts เป็น proxy.ts Supabase ให้ใช้ getClaims() แทน getSession() ในโค้ดฝั่ง server และ key แบบเก่า (anon, service_role) กำลังถูกแทนด้วย publishable key กับ secret key บทความนี้ใช้วิธีปัจจุบันทั้งหมด อ้างอิงจากเอกสาร Supabase ที่อัปเดตเดือนตุลาคม 2026
ภาพรวมก่อนเริ่ม
#Next.js#Supabase#LINE Login#Authentication
lib/supabase/client.ts
| สร้าง client สำหรับ Client Component เช่น ฟังข้อมูลแบบเรียลไทม์ |
lib/supabase/server.ts
| สร้าง client สำหรับ Server Component, Server Action และ Route Handler |
lib/supabase/proxy.ts
| ต่ออายุ token และเขียน cookie ใหม่ |
proxy.ts (root)
| เรียกไฟล์ด้านบนทุก request (Next.js 15 ลงไปใช้ middleware.ts) |
app/auth/callback/route.ts
| รับผู้ใช้กลับมาจาก Google หรือ LINE |
ที่ต้องมี proxy เพราะ Server Component เขียน cookie ไม่ได้ ถ้าไม่มีไฟล์นี้ token จะหมดอายุแล้วไม่มีใครต่อให้ ผู้ใช้จะหลุดออกจากระบบเองหลังผ่านไปสักพัก
ขั้นที่ 1: ติดตั้งและตั้ง env
npm install @supabase/supabase-js @supabase/ssr
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxxx.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_xxxx
หา key ได้ที่หน้า API Keys ของโปรเจกต์ เอกสาร Supabase ระบุว่า key แบบเก่าจะเลิกใช้ภายในสิ้นปี 2026 ถ้าโปรเจกต์ยังใช้ anon key อยู่ ให้สร้าง publishable key ใหม่แล้วเปลี่ยนตั้งแต่ตอนนี้ ส่วน secret key (sb_secret_...) ใช้ฝั่ง server เท่านั้น ห้ามตั้งชื่อตัวแปรให้ขึ้นต้นด้วย NEXT_PUBLIC_
ขั้นที่ 2: สร้าง client สองแบบ
// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!
)
}
// lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// เรียกจาก Server Component ซึ่งเขียน cookie ไม่ได้ ปล่อยให้ proxy ทำแทน
}
},
},
}
)
}
สร้าง client ใหม่ทุกครั้งที่ใช้ อย่าสร้างครั้งเดียวแล้วเก็บเป็นตัวแปรกลาง เพราะ client ฝั่ง server ผูกกับ cookie ของ request นั้น ถ้าใช้ร่วมกัน ผู้ใช้คนหนึ่งอาจได้ session ของอีกคน
ขั้นที่ 3: ตั้ง proxy ให้ต่ออายุ token
// lib/supabase/proxy.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function updateSession(request: NextRequest) {
let supabaseResponse = NextResponse.next({ request })
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))
supabaseResponse = NextResponse.next({ request })
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
}
)
// ห้ามมีโค้ดอื่นคั่นระหว่างสร้าง client กับบรรทัดนี้
const { data } = await supabase.auth.getClaims()
const isLoggedIn = !!data?.claims
const path = request.nextUrl.pathname
if (!isLoggedIn && path.startsWith('/account')) {
const url = request.nextUrl.clone()
url.pathname = '/login'
return NextResponse.redirect(url)
}
return supabaseResponse
}
// proxy.ts (ที่ root ของโปรเจกต์)
import { type NextRequest } from 'next/server'
import { updateSession } from '@/lib/supabase/proxy'
export async function proxy(request: NextRequest) {
return await updateSession(request)
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
ถ้ายังใช้ Next.js 15 หรือต่ำกว่า ให้ตั้งชื่อไฟล์เป็น middleware.ts และเปลี่ยนชื่อฟังก์ชันเป็น middleware โค้ดข้างในเหมือนเดิม เอกสาร Supabase เตือนว่า Next.js 15 จะไม่เรียกไฟล์ proxy.ts เลย session จึงไม่ถูกต่ออายุ ผู้ใช้จะหลุดออกจากระบบโดยไม่มี error ให้เห็น
อีกข้อที่เอกสารย้ำคือ ต้องคืน supabaseResponse ตัวล่าสุดที่ setAll สร้างไว้ ถ้าสร้าง response ใหม่เองโดยไม่คัดลอก cookie ไปด้วย token ที่ต่ออายุแล้วจะหายไป
ขั้นที่ 4: หน้าล็อกอินด้วยอีเมลและรหัสผ่าน
ใช้ Server Action ทำให้ไม่ต้องเขียน API แยก และรหัสผ่านไม่ต้องผ่านโค้ดฝั่งเบราว์เซอร์ของเราเลย
// app/login/actions.ts
'use server'
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'
export async function login(formData: FormData) {
const supabase = await createClient()
const { error } = await supabase.auth.signInWithPassword({
email: String(formData.get('email')),
password: String(formData.get('password')),
})
if (error) redirect('/login?error=1')
redirect('/account')
}
export async function signup(formData: FormData) {
const supabase = await createClient()
const { error } = await supabase.auth.signUp({
email: String(formData.get('email')),
password: String(formData.get('password')),
})
if (error) redirect('/login?error=1')
redirect('/login?check-email=1')
}
// app/login/page.tsx
import { login, signup } from './actions'
export default function LoginPage() {
return (
<form>
<input name="email" type="email" required placeholder="อีเมล" />
<input name="password" type="password" required placeholder="รหัสผ่าน" />
<button formAction={login}>เข้าสู่ระบบ</button>
<button formAction={signup}>สมัครสมาชิก</button>
</form>
)
}
โดยค่าเริ่มต้น Supabase จะส่งอีเมลยืนยันก่อนให้ล็อกอินได้ ตอนทดสอบในเครื่องปิดได้ที่การตั้งค่า Email provider แต่ก่อนเปิดใช้จริงควรเปิดกลับ และตั้ง Custom SMTP เพราะระบบส่งอีเมลในตัวของ Supabase จำกัดจำนวนต่อชั่วโมงไว้ต่ำมาก ไม่เหมาะกับ production
ขั้นที่ 5: ป้องกันหน้าที่ต้องล็อกอิน
// app/account/page.tsx
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'
export default async function AccountPage() {
const supabase = await createClient()
const { data } = await supabase.auth.getClaims()
if (!data?.claims) redirect('/login')
return <p>เข้าสู่ระบบด้วย {data.claims.email}</p>
}
ทำไมต้องเป็น getClaims() เอกสาร Supabase อธิบายว่าใครก็ปลอม cookie session ได้ ถ้าเชื่อ cookie โดยไม่ตรวจ คนอื่นอาจเปิดหน้าของผู้ใช้คนอื่นได้ getClaims() ตรวจลายเซ็นของ token ทุกครั้ง ส่วน getSession() อ่านค่าจาก cookie ตรง ๆ โดยไม่ตรวจซ้ำ จึงห้ามใช้ตัดสินสิทธิ์ในโค้ดฝั่ง server ถ้าต้องการข้อมูลผู้ใช้ล่าสุดจากระบบ ให้ใช้ getUser() ซึ่งเรียกไปที่ Auth server ทุกครั้ง
ขั้นที่ 6: ล็อกอินด้วย Google หรือ LINE
ปุ่ม OAuth เรียก signInWithOAuth แล้วให้ provider ส่งผู้ใช้กลับมาที่ callback ของเรา
// components/oauth-buttons.tsx
'use client'
import { createClient } from '@/lib/supabase/client'
export default function OAuthButtons() {
const supabase = createClient()
const signIn = (provider: string) =>
supabase.auth.signInWithOAuth({
provider: provider as any,
options: { redirectTo: `${location.origin}/auth/callback?next=/account` },
})
return (
<>
<button onClick={() => signIn('google')}>เข้าสู่ระบบด้วย Google</button>
<button onClick={() => signIn('custom:line')}>เข้าสู่ระบบด้วย LINE</button>
</>
)
}
// app/auth/callback/route.ts
import { NextResponse, type NextRequest } from 'next/server'
import { createClient } from '@/lib/supabase/server'
export async function GET(request: NextRequest) {
const { searchParams, origin } = request.nextUrl
const code = searchParams.get('code')
const next = searchParams.get('next') ?? '/account'
if (code) {
const supabase = await createClient()
const { error } = await supabase.auth.exchangeCodeForSession(code)
if (!error) return NextResponse.redirect(`${origin}${next}`)
}
return NextResponse.redirect(`${origin}/login?error=oauth`)
}
LINE Login ผ่าน Supabase ด้วย Custom OIDC Provider
Supabase ไม่มี LINE ในรายชื่อ provider สำเร็จรูป แต่ตอนนี้มีฟีเจอร์ Custom OAuth/OIDC Providers ที่เพิ่ม provider ที่รองรับ OpenID Connect ได้เอง ซึ่ง LINE Login v2.1 รองรับอยู่แล้ว แพ็กเกจฟรีเพิ่มได้ 3 provider แพ็กเกจ Pro ขึ้นไปเพิ่มได้ไม่จำกัด
- 1.ใน Supabase ไปที่ Auth Providers กด New Provider แล้วเลือก Auto-discovery (OIDC)
- 2.ตั้งชื่อเป็น
custom:line ใส่ Channel ID และ Channel secret จาก LINE Developers Console - 3.ใส่ Issuer URL ของ LINE คือ
https://access.line.me - 4.คัดลอก Callback URL ที่ Supabase แสดง ไปใส่ในแท็บ LINE Login ของ channel
- 5.เปิดตัวเลือก
email_optional เพราะผู้ใช้ LINE หลายคนไม่ได้ให้อีเมล ถ้าไม่เปิด คนกลุ่มนี้จะล็อกอินไม่ได้ - 6.ถ้าอยากให้ผู้ใช้เพิ่มเพื่อน LINE OA ตอนล็อกอิน ใส่
bot_prompt: "normal" ใน Authorization params
ทุก provider ต้องเพิ่ม URL ของเว็บใน Authentication → URL Configuration ทั้ง Site URL และ Redirect URLs เช่น http://localhost:3000/** และ https://www.example.com/** ถ้าใช้ลิงก์ preview ของ Vercel ให้เพิ่ม URL แบบ wildcard ของทีมด้วย ไม่อย่างนั้นล็อกอินบนลิงก์ preview จะเด้งกลับไปที่ Site URL
ขั้นที่ 7: เปิด Row Level Security ให้ข้อมูลผู้ใช้
ระบบล็อกอินบอกได้แค่ว่าใครเป็นใคร ส่วนที่กันไม่ให้ผู้ใช้เห็นข้อมูลของคนอื่นคือ Row Level Security (RLS) ที่ตั้งในฐานข้อมูล ตัวอย่างตารางโปรไฟล์ที่แต่ละคนเห็นและแก้ได้แค่ของตัวเอง:
create table profiles (
id uuid primary key references auth.users on delete cascade,
display_name text,
updated_at timestamptz default now()
);
alter table profiles enable row level security;
create policy "อ่านโปรไฟล์ตัวเอง"
on profiles for select to authenticated
using ((select auth.uid()) = id);
create policy "แก้โปรไฟล์ตัวเอง"
on profiles for update to authenticated
using ((select auth.uid()) = id)
with check ((select auth.uid()) = id);
ทุกตารางที่เปิดให้เรียกจากเบราว์เซอร์ต้องเปิด RLS เพราะ publishable key อยู่ในโค้ดฝั่งเบราว์เซอร์ ใครก็เปิดดูได้ ตารางที่ไม่ได้เปิด RLS จึงเท่ากับเปิดให้ทุกคนอ่านเขียนได้ ปัญหานี้พบบ่อยมากในแอปที่สร้างด้วย AI ซึ่งอธิบายไว้ใน บทความ Vibe coding คืออะไร
ปัญหาที่เจอบ่อยกับ Supabase Auth Next.js
ล็อกอินแล้วหลุดเองหลังผ่านไปสักพัก ส่วนใหญ่เป็นเพราะ proxy ไม่ทำงาน เช่น ใช้ Next.js 15 แต่ตั้งชื่อไฟล์เป็น proxy.ts หรือ matcher ไม่ครอบหน้านั้น หรือคืน response ที่ไม่มี cookie ใหม่
ล็อกอินผ่าน Google แล้วเด้งไปหน้าแรกของเว็บจริง ทั้งที่ทดสอบบน localhost URL ที่ส่งใน redirectTo ไม่อยู่ใน Redirect URLs Supabase จึงใช้ Site URL แทน
ผู้ใช้เห็นข้อมูลของคนอื่น ตรวจ RLS ก่อนเป็นอย่างแรก ถ้าเปิดแล้ว ให้ดูว่าหน้านั้นถูก cache ด้วย ISR หรือ CDN หรือเปล่า เอกสาร Supabase เตือนว่า ถ้า response ที่มี cookie session ถูก cache ไว้แล้วส่งให้คนอื่น คนนั้นจะเข้าระบบเป็นผู้ใช้ผิดคน หน้าที่ขึ้นกับผู้ใช้ควร render แบบ dynamic เสมอ
สมัครแล้วไม่ได้รับอีเมลยืนยัน ระบบส่งอีเมลในตัวจำกัดจำนวนไว้ ให้ตั้ง Custom SMTP และเช็กโฟลเดอร์สแปม
deploy แล้วล็อกอินไม่ได้ ทั้งที่ในเครื่องได้ ลืมใส่ env ใน Vercel หรือยังไม่ได้เพิ่มโดเมนจริงใน Redirect URLs ขั้นตอนตั้ง env บน Vercel อยู่ใน บทความ Deploy Next.js บน Vercel
คำถามที่พบบ่อย
แพ็กเกจฟรีรองรับผู้ใช้ที่ล็อกอินได้ 50,000 คนต่อเดือน แพ็กเกจ Pro เพิ่มเป็น 100,000 คน รายละเอียดราคาอยู่ในบทความ Supabase vs Firebase
แพ็กเกจนี้ถูกแทนด้วย @supabase/ssr แล้ว Supabase มีคู่มือย้ายจาก Auth Helpers ในเอกสาร โปรเจกต์ใหม่ควรใช้ @supabase/ssr ตั้งแต่แรก
ได้และปลอดภัย แต่ getUser() เรียก Auth server ทุกครั้ง ทำให้ช้ากว่า ส่วน getClaims() ในโปรเจกต์ที่ใช้ asymmetric signing key (ค่าเริ่มต้นของโปรเจกต์ใหม่) ตรวจลายเซ็นในเครื่องได้เลย ใช้ getUser() เฉพาะตอนที่ต้องการข้อมูลผู้ใช้ล่าสุด เช่น หลังผู้ใช้แก้อีเมล
ได้ @supabase/ssr รองรับทั้งสองแบบ แต่โค้ดสร้าง client ฝั่ง server ต่างกัน ดูแท็บ Pages Router ในเอกสารของ Supabase
ส่วนล็อกอินตามบทความนี้ทำได้ในวันเดียว ส่วนที่ใช้เวลาจริงคือการออกแบบตารางและ RLS ให้ถูกต้องตามสิทธิ์ของผู้ใช้แต่ละแบบ เช่น สมาชิก พนักงาน และแอดมิน
สรุป
ตั้ง Supabase Auth ใน Next.js ปี 2026 ให้ใช้ @supabase/ssr มี client สองตัวกับ proxy หนึ่งไฟล์ ใช้ publishable key เช็กผู้ใช้ฝั่ง server ด้วย getClaims() เสมอ และเปิด RLS ทุกตาราง ถ้าต้องการ LINE Login ตอนนี้ทำผ่าน Custom OIDC Provider ได้เลยโดยไม่ต้องมีไลบรารีล็อกอินแยก
ถ้าอยากได้ระบบสมาชิกที่ทำด้วย Next.js และ Supabase พร้อมล็อกอิน LINE และตั้งสิทธิ์ข้อมูลให้ครบ ดูรายละเอียดได้ที่ บริการรับทำ Web Application