ลองเล่น Supabase กับ Next.js กัน
/ 12 min read
สามารถดู video ของหัวข้อนี้ก่อนได้ ดู video
Supabase คืออะไร ?
Ref: https://supabase.com/docs

Supabase is an open source Firebase alternative. Start your project with a Postgres database, Authentication, instant APIs, Edge Functions, Realtime subscriptions, Storage, and Vector embeddings.
Supabase คือ open source platform ที่ทำการเตรียมเครื่องมือให้นักพัฒนาสามารถขึ้นงาน application ได้อย่างรวดเร็วขึ้น ซึ่งหลายๆคนจะมองว่า Supabase มีลักษณะใกล้เคียงกันกับ Firebase เลย (มี Service หลายๆตัวใกล้เคียงกันด้วย) โดย feature หลักๆของ Supabase คือ
- Database ใช้ PostgreSQL ซึ่งถือว่าเป็น DB สุดทรงพลังที่สามารถ query ข้อมูลได้หลากหลายประเภท รวมถึงสามารถทำ complex queries, relation (ตามคุณสมบัติของ SQL) ได้
- มี Authentication service ที่สามารถทำ Service Auth เอง หรือจะใช้ร่วมกับ 3rd party อย่าง Google, GitHub, Facebook, X (Twitter) ก็ได้
- มี Feature realtime ที่สามารถดึงข้อมูลแบบ realtime เมื่อมี change กับ data เกิดขึ้นได้
- มี Storage ที่สามารถเก็บไฟล์ขนาดใหญ่เช่น ภาพ, video หรือไฟล์ประเภทต่างๆที่อนุญาตให้ user upload เข้ามาได้
- มี API ที่ supabase ได้ทำการ generate ออกมาเพื่อให้สามารถต่อไปยัง database เพื่อให้สามารถจัดการ CRUD (Create, Read, Update, Delete) กับ Database ได้ง่ายขึ้น
- มี Edge Functions ที่ทำให้ supabase สามารถ run code แบบ server-side code ออกมาได้
- มี Dashboard สามารถจัดการกับทุก Service ของ supabase ได้ (รวมถึงสามารถใช้คำสั่ง query ใน Dashboard ได้)
จุดเด่นจริงๆเลยของ Supabase คือ “เชื่อมง่าย” และ “ตัวอย่างเยอะ” เนื่องจากเอกสารแทบจะเตรียมวิธีไว้ให้หมดแล้วว่าวิธีเชื่อมแต่ละอย่าง วิธีใช้งานแต่ละอันสามารถทำอย่างไรได้บ้าง เดี๋ยวเรามาเรียนรู้ไปพร้อมๆกันในบทความนี้
และอีกจุดหนึ่ง (ซึ่งเป็นจุดชูโรงที่ Supabase เทียบกับ Firebase ด้วย) คือการ “ไม่คิด pricing ตาม request”

Ref: https://supabase.com/pricing
ถ้าสังเกตตรงหน้า Pricing ของ supabase จะเห็นว่า supabase ใช้ประโยคนี้เป็นประโยคแรกในหัวข้อ Free “Unlimited API requests” นั่นคือ สามารถยิง Request เท่าไหร่ก็ได้เลยไม่จำกัด แต่จะไปจำกัดส่วนที่เป็น Brandwidth, Concurrent หรือ ขนาดข้อมูลแทน เลยเป็นอีกจุดพิจารณาสำหรับการเลือกใช้ระหว่าง Firebase และ Supabase เช่นเดียวกันว่า ชอบการคิด Pricing แบบไหนมากกว่ากัน
Next.js เราจะเลือกนาย
Ref: https://supabase.com/docs/guides/getting-started/quickstarts/nextjs
เพื่อให้ทุกคนรู้จักกับ Supabase มากขึ้น เราจะพามาเล่นผ่าน Framework Next.js
กัน เนื่องจากเป็น Framework ที่ผู้ติดตามของเรามีคนใช้มากพอสมควร รวมถึงตัวอย่างของ Next.js
ครอบคลุมทั้งฝั่งของ Client side (ที่เป็น React) และ ฝั่งของ Server side (ที่เป็น Node.js ที่ Next ห่อไป) จะได้เห็นภาพการใช้งานทั้ง 2 ฝั่งไปพร้อมๆกันได้
ตัวอย่างที่เราจะหยิบมาทำนั้น เป็นตัวอย่างที่ใช้สารตั้งต้นตาม quichstart next.js
ของ supabase เลย โดยจะทำการดัดแปลงบางส่วน และเพิ่ม query บางส่วน (ที่เกี่ยวข้องตามโจทย์เรา) เข้าไปแทน
ซึ่ง ตัวอย่างนอกเหนือจาก quickstart แล้วใน supabase มีการเตรียมอีกตัวอย่างหนึ่งไว้คือ https://supabase.com/docs/guides/auth/managing-user-data โดยมีตัวอย่างจัดการ Permission และมีตัวอย่างเรื่องของ Login ด้วย Magic Link อยู่ด้วย (เพื่อใครสนใจ usecase นี้ สามารถเข้าไปอ่านเพิ่มเติมได้)
ดังนั้นเพื่อให้เกิดความเข้าใจที่ถ่องแท้ ขอให้รู้จัก Next.js
มาก่อนอ่าน (หรือดู video) ของบทความนี้ด้วย หากใครยังไม่รู้จัก Next.js มาก่อน ขอแนะนำ อ่านที่นี่ เลย
เราจะทำอะไรกันบ้าง
เราจะทำระบบลงทะเบียนกัน โดยเราจะแยกออกเป็น 2 ฝั่งคือ
- ฝั่งหน้าบ้าน (สำหรับผู้ใช้ทั่วไป)
- สามารถกรอกฟอร์มเพื่อ submit ข้อมูลเข้ามาได้ โดยข้อมูลจะประกอบด้วย fullname (ชื่อ), email และ tel (เบอร์โทรศัพท์)
- สามารถแนบ upload ไฟล์มาได้ (จะนำไปเก็บไว้ใน supabase storage)
ให้อารมณ์เหมือนทำหน้าเว็บพร้อมกรอกใบสมัครพร้อมเอกสารเข้ามา
- ฝั่งหลังบ้าน (สำหรับ user ที่ login)
- แสดง user ทั้งหมดที่ทำการกรอกเข้าระบบมา สามารถดูไฟล์แนบเขาได้
- ทำ Search ได้ รวมถึงสามารถทำ pagination ได้
เราจะทำตัวอย่างประมาณนี้กัน let’s code ทุกคน
เริ่มต้น code กัน
เริ่มต้น project ตาม guideline ของ supabase เลย โดยการ run ผ่าน command
npx create-next-app -e with-supabase
หลังจากนั้น หยิบ key จาก dashboard วางใน .env.local
ตามใน Dashboard ของ supabase
NEXT_PUBLIC_SUPABASE_URL=your-project-urlNEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
โดยมันจะอยู่ในส่วนของ Project Setting
> API
จะมีแสดงส่วน key ออกมาอยู่

ให้นำ key ตรงตำแหน่ง ANON มาใส่ NEXT_PUBLIC_SUPABASE_ANON_KEY
และ URL มาใส่ NEXT_PUBLIC_SUPABASE_URL
เสร็จแล้ว ลอง run project ดูด้วยคำสั่ง
npm run dev
ก็จะเจอว่าสามารถที่จะใช้งานได้ทันที
- สามารถใช้งานส่วนของ login / logout ออกมาได้เลย เราจะคง code ส่วนนี้ไว้ สามารถนำไปใช้ดูเป็นตัวอย่างได้ เนื่องจากเป็น pattern การทำ authentication email / password ของ supabase อยู่แล้ว
โดยนี่คือ structure ที่ CLI ของ next template supabase ได้เตรียมไว้ให้ โดย เราจะทำการลบให้เหลือแค่่ส่วนที่จำเป็นตาม structure ด้านล่างนี้
.├── app│ ├── auth│ │ └── callback --> ส่วนที่รับมาหลังจาก login เสร็จ (ตัวอย่างรับจาก API)│ │ └── route.ts│ ├── globals.css│ ├── layout.tsx│ ├── login│ │ └── page.tsx --> หน้า login│ ├── page.tsx --> หน้าหลัก│ ├── actions.ts --> ส่วนสำหรับรับ register│ └── users --> หน้าสำหรับจัดการ user│ └── page.tsx├── components│ ├── AuthButton.tsx ส่วนสำหรับจัดการ Authentication ส่วน header│ ├── UserManagement.tsx├── middleware.ts├── next.config.js├── package.json├── postcss.config.js├── tailwind.config.js --> ลง tailwind ไว้แล้ว├── tsconfig.json└── utils --> ทำการเตรียม utils function ที่สามารถเรียกใช้งาน supabase ไว้ได้ └── supabase ├── client.ts ├── middleware.ts └── server.ts
ส่วนที่สำคัญๆของ Structure นี้คือ ตัวที่อยู่ใน utils
เป็นตัวที่ supabase CLI เตรียมเอาไว้ให้ เพื่อให้เราสามารถ import คำสั่ง supabase ผ่าน folder utils
ได้ทันที (ตามชื่อ folder เลย)
** สามารถดูตัวอย่างได้จากการ import supabase ใน code หลังจากนี้ เดี๋ยวเราจะค่อยๆไล่ทำทีละ step กัน
ก่อนจะเริ่ม code ต้องสร้าง schema ก่อน
สิ่งที่เราจะต้องทำที่ supabase ก่อนคือ
- ต้องสร้าง table สำหรับเก็บข้อมูล user เอาไว้ก่อน (ซึ่งเราจะตั้งชื่อว่า
users
) - ต้องสร้าง storage สำหรับเก็บไฟล์ที่ user upload เข้ามา (ซึ่งเราจะตั้งชื่อว่า
attachments
)
โดยทั้ง 2 อย่างนี้สามารถสร้างได้ผ่าน dashboard supabase ได้เลย
สร้าง table
สามารถเพิ่ม table ได้จาก Table Editor

โดยสามารถทำได้ 2 วิธีคือ 1. ทำผ่าน UI (สามารถกดได้ผ่าน New Table
แล้วใส่ประเภทข้อมูลตามภาพนี้ได้เลย และตั้งชื่อว่า users
)

หรือ 2. ทำผ่าน query โดยนำ SQL นี้ไปใส่ตรง SQL Editor
ของ Dashboard supabase ก็จะสามารถสร้าง table ได้เหมือนกัน (ถ้าใครเคยใช้ MySQL เหมือนเวลาเราใช้ phpmyadmin เลย)
create table public.users ( id bigint generated by default as identity, fullname text not null, email text null, tel text null, attachment text null, constraint users_pkey primary key (id), constraint users_email_key unique (email) ) tablespace pg_default;
เมื่อสร้างเรียบร้อยกลับมาที่เมนู Table Editor จะต้องเจอ table users

สร้าง storage
สำหรับการสร้าง storage ให้ทำการกดสร้างจากเมนู Storage และกดสร้างเป็น Bucket ใหม่โดยตั้งชื่อว่า attachments
และเปิดเป็น Public bucket เพื่อให้สามารถเปิด url เป็น Public url ออกมาได้

เมื่อสร้างเรียบร้อยกลับมาที่เมนู Storage จะต้องเจอ attachment อยู่

กำหนด Policy
Ref: https://supabase.com/docs/guides/auth/row-level-security
Postgres Row Level Security (RLS) คือ feature ของ Postgres ที่อนุญาตให้เราสามารถควบคุมการจัดการคำสั่งไม่ว่าจะเป็น SELECT/INSERT/UPDATE/DELETE
statement ใน table ของ Postgres ได้ เช่น
- สามารถกำหนดได้ว่า user ที่ login เท่านั้นสามารถ UPDATE ได้
- user ที่ต้องอยู่ใน table author เท่านั้นถึงจะสามารถ SELECT ได้ เป็นต้น
ซึ่งด้วย feature ของ RLS นั้นจะ บังคับ ให้ table ใน Postgres ต้องมี Policy ก่อนถึงจะสามารถจัดการกับข้อมูลได้ (คือต้องอนุญาตถึงจะสามารถทำได้ แทนที่จะทำได้เลยเป็น default)
ซึ่งใน Supabase นั้น สามารถใช้ RLS ได้ผ่าน feature ที่ชื่อ Policies
(https://www.postgresql.org/docs/current/sql-createpolicy.html) โดย supabase นั้นมีหน้า UI ที่สามารถใส่ policy เข้าไปผ่าน Dashboard ได้ ไม่ว่าจะเป็นทั้ง table schema หรือ storage เราจะมาเริ่มใส่ Policy ตามโจทย์กัน
ใส่ Policy table users
โจทย์ของ Table users คือ
- มีแค่เฉพาะ คนที่ login เท่านั้น ถึงจะสามารถดูข้อมูล user ทุกคนได้
- บุคคลทั่วไป สามารถส่งข้อมูลเข้ามาเพิ่มได้ แต่ แก้ไข, ลบ ไม่ได้
โดยเราสามารถเพิ่ม Policy ได้จากการกด ลูกศรลง ตรง table และเลือกตรง View Policy
จะสามารถมายังหน้าของ Policy ได้

หลังจากนั้นกด New Policy
เพื่อเพิ่ม Policy เข้าไปใหม่

จากโจทย์เรานั้น Policy ที่เราจะเพิ่มเข้าไปจะมีด้วยกัน 2 Policy คือ
SELECT
สำหรับ user ที่authenticated

INSERT
สำหรับ user แบบ public ทั่วไป

และนี่คือ Policy ทั้งหมดของโจทย์นี้

ใส่ Policy storage attachments
โจทย์ของ Storage attachment คือ
- มีแค่เฉพาะ คนที่ login เท่านั้น ถึงจะสามารถดูข้อมูลไฟล์ที่ upload เข้ามาได้
- บุคคลทั่วไป สามารถส่งข้อมูลเข้ามาเพิ่มได้ แต่ แก้ไข, ลบ รวมถึงกลับมาดูไม่ได้
โดย Storage นั้นจะสามารถเพิิ่มได้จากหัวข้อ Storage > Policies

จากโจทย์เรานั้น Policy ที่เราจะเพิ่มเข้าไปจะมีด้วยกัน 2 Policy คือ
SELECT
สำหรับ user ที่authenticated

INSERT
สำหรับ user แบบ public ทั่วไป

และนี่คือ Policy ทั้งหมดของ Storage

เพียงเท่านี้ ทั้ง Table และ Storage เราก็จะพร้อมสำหรับการเริ่มงานเป็นที่เรียบร้อย step ต่อไปเราจะมาเริ่ม code กัน
1. ฝั่งหน้าบ้าน
ไฟล์ที่เกี่ยวข้อง
.├── app│ ├── page.tsx --> หน้าหลัก│ ├── actions.ts --> ส่วนสำหรับรับ register
ตามที่เราอธิบายไว้ตอนแรก โจทย์หลักของฝั่งหน้าบ้านจะมี 2 โจทย์คือ
- สามารถกรอกฟอร์มเพื่อ submit ข้อมูลเข้ามาได้ โดยข้อมูลจะประกอบด้วย fullname (ชื่อ), email และ tel (เบอร์โทรศัพท์)
- สามารถแนบ upload ไฟล์มาได้ (จะนำไปเก็บไว้ใน supabase storage)
ผลลัพธ์จะได้ออกมาประมาณนี้

เราจะเริ่มแก้ไขที่ละไฟล์กัน
ที่ page.tsx
สิ่งที่เราจะเพิ่ม
- ทำการสร้าง Form สำหรับกรอก fullname, email, tel ขึ้นมา
- ปรับ component นี้เป็น
Client component
(เพื่อให้สามารถ handlr state ของ error message ด้วยคำสั่งของ React ได้) - ทำการ import
register()
ซึ่งจะเป็น Server action ที่ทำการต่อไปยัง server ตอน submit form - ทำการเชื่อม state ของการ submit form กับ Server action
register()
เพื่อให้สามารถแสดงผล state ผ่านหน้าจอออกมาได้ (แสดงข้อความสำเร็จ หรือมีปัญหาออกมาได้)
'use client'
import { useFormState } from 'react-dom'import { register } from './actions' // เดี๋ยวเราไปเขียนเพิ่ม
const initialState = { success: false, message: null,}
export default async function Index() { const [state, formAction] = useFormState(register, initialState)
return ( <div className="flex-1 w-full flex flex-col gap-20 items-center"> <div className="animate-in flex-1 flex flex-col gap-20 opacity-0 max-w-4xl px-3"> <main className="flex-1 flex flex-col gap-6"> <form className="animate-in flex-1 flex flex-col w-full justify-center gap-2 text-foreground" action={formAction} > <div>Register Form</div> <input className="rounded-md px-4 py-2 bg-inherit border mb-6" type="text" name="fullname" placeholder="Fullname" /> <input className="rounded-md px-4 py-2 bg-inherit border mb-6" type="text" name="email" placeholder="Email" /> <input className="rounded-md px-4 py-2 bg-inherit border mb-6" type="text" name="tel" placeholder="Tel" /> <input className="rounded-md px-4 py-2 bg-inherit border mb-6" type="file" name="attachment" /> <button className="bg-green-700 rounded-md px-4 py-2 text-foreground mb-2"> Submit Form </button>
{state.message && <div>Error: {state.message}</div>} {state.success && ( <div className="bg-green-500 p-4">Register Successful !</div> )} </form> </main> </div> </div> )}
ที่ actions.ts
สิ่งที่เราจะเพิ่ม
- ทำการเพิ่ม function Server action
register()
เข้ามาสำหรับรับค่าจาก Form ของฝั่ง Client component - ทำการอ่านค่า fullname, email, tel, attachment ผ่าน
formData
เข้ามา - อ่านค่าจาก attachment และดำเนินการ upload เข้า storage ก่อน โดยจะทำการเปลี่ยนชื่อไฟล์ที่ generate ใหม่ด้วย
uuid()
เพื่อให้ชื่อไฟล์ไม่ซ้ำกัน ผ่านคำสั่งsupabase.storage.from('attachments').upload(uuidFileName, attachment)
- หลังจาก upload เสร็จให้เอา public path มา (เพื่อเก็บไว้ใน database) ผ่านคำสั่ง
supabase.storage.from('attachments').getPublicUrl(uuidFileName)
- นำข้อมูลทั้งหมดมาประกอบกัน โดยทำการ insert ข้อมูลเข้า table
users
ผ่านคำสั่งsupabase.from('users').insert()
- เมื่อเรียบร้อยให้ส่ง message กลับไปบอกผ่าน form ว่าบันทึกข้อมูลสำเร็จ (และส่ง message กลับไปบอกใน case ที่มี Error เกิดขึ้น)
'use server'
import { createClient } from '@/utils/supabase/server'import { cookies } from 'next/headers'import { v4 as uuidv4 } from 'uuid'
export async function register(prevState: any, formData: FormData) { try { const fullname = formData.get('fullname') as string const email = formData.get('email') as string const tel = formData.get('tel') as string
// get formData from input type file name 'attachment' const attachment = formData.get('attachment') as File
const cookieStore = cookies() const supabase = createClient(cookieStore)
// // upload attachment to supabase storage const uuidFileName = uuidv4()
const { error } = await supabase.storage .from('attachments') .upload(uuidFileName, attachment)
if (error) { console.log('error upload file', error) return { message: 'cannot upload attachment' } }
const { data: attachmentUrl } = await supabase.storage .from('attachments') .getPublicUrl(uuidFileName)
// insert data to supabase table users with these 4 fields const { error: insertError } = await supabase.from('users').insert({ fullname, email, tel, attachment: attachmentUrl.publicUrl, })
if (insertError) { console.log('error', insertError) return { message: 'Could not register user' } }
// success case when update correct return { success: true } } catch (error) { console.log('server error', error) return { message: 'Server error' } }}
2. ฝั่งหลังบ้าน
จากด้านบน โจทย์ของฝั่งหลังบ้านคือ
- แสดง user ทั้งหมดที่ทำการกรอกเข้าระบบมา สามารถดูไฟล์แนบเขาได้
- ทำ Search ได้ รวมถึงสามารถทำ pagination ได้
ไฟล์ที่เกี่ยวข้อง
├── app│ ├── login│ │ └── page.tsx│ └── users│ └── page.tsx├── components│ ├── AuthButton.tsx│ └── UserManagement.tsx├── middleware.ts
ผลลัพธ์จะออกมาเป็นแบบนี้
ที่ login/page.tsx
อย่างแรก
- หน้า login ไม่ต้องทำอะไร สามารถใช้ตามตัวเดิมได้ แต่ปรับเรื่อง link และตัดส่วน Back ออกพอ
- code จะคล้ายๆกับที่ CLI generate มาให้แล้ว
import { headers, cookies } from 'next/headers'import { createClient } from '@/utils/supabase/server'import { redirect } from 'next/navigation'
export default function Login({ searchParams,}: { searchParams: { message: string }}) { const signIn = async (formData: FormData) => { 'use server'
const email = formData.get('email') as string const password = formData.get('password') as string const cookieStore = cookies() const supabase = createClient(cookieStore)
const { error } = await supabase.auth.signInWithPassword({ email, password, })
if (error) { return redirect('/login?message=Could not authenticate user') }
return redirect('/users') }
const signUp = async (formData: FormData) => { 'use server'
const origin = headers().get('origin') const email = formData.get('email') as string const password = formData.get('password') as string const cookieStore = cookies() const supabase = createClient(cookieStore)
const { error } = await supabase.auth.signUp({ email, password, options: { emailRedirectTo: `${origin}/auth/callback`, }, })
if (error) { return redirect('/login?message=Could not authenticate user') }
return redirect('/login?message=Check email to continue sign in process') }
return ( <div className="flex-1 flex flex-col w-full px-8 sm:max-w-md justify-center gap-2"> <form className="animate-in flex-1 flex flex-col w-full justify-center gap-2 text-foreground" action={signIn} > <label className="text-md" htmlFor="email"> Email </label> <input className="rounded-md px-4 py-2 bg-inherit border mb-6" name="email" required /> <label className="text-md" htmlFor="password"> Password </label> <input className="rounded-md px-4 py-2 bg-inherit border mb-6" type="password" name="password" placeholder="••••••••" required /> <button className="bg-green-700 rounded-md px-4 py-2 text-foreground mb-2"> Sign In </button> <button formAction={signUp} className="border border-foreground/20 rounded-md px-4 py-2 text-foreground mb-2" > Sign Up </button> {searchParams?.message && ( <p className="mt-4 p-4 bg-foreground/10 text-foreground text-center"> {searchParams.message} </p> )} </form> </div> )}
ที่ user/page.tsx
สิ่งที่เราจะทำในหน้านี้คือ
- ทำการเพิ่มการเรียก
<AuthButton />
(ส่วนที่มีการเชื่อม login เอาไว้ที่ CLI ทำการ generate เอาไว้) เพื่อใช้สำหรับการ handle state login (ในกรณีที่ยังไม่ login จะได้สามารถกลับไปยังหน้า login ได้) - เรียกใช้
<UserManagement />
ซึ่งเป็นส่วนสำหรับการแสดงผลข้อมูล user ทั้งหมดของระบบออกมา แยกส่วนออกเป็น Client Component เนื่องจากเราจะมีการใช้งานร่วมกับ state ของ Client (ส่งผลทำให้ไม่สามารถใช้งานจากบน Server Component โดยตรงได้)
import AuthButton from '../../components/AuthButton'import UserManagement from '../../components/UserManagement'
export default async function Index() {
return ( <div className="flex-1 w-full flex flex-col gap-20 items-center"> <nav className="w-full flex justify-center border-b border-b-foreground/10 h-16"> <div className="w-full max-w-4xl flex justify-between items-center p-3 text-sm"> <AuthButton /> </div> </nav>
<div className="animate-in flex-1 flex flex-col gap-20 opacity-0 px-3"> <main className="flex-1 flex flex-col gap-6"> <UserManagement /> </main> </div> </div> )}
Step ต่อไปเราจะไป implement ตัว UserManagement.tsx
ที่เป็นตัวหลักกันต่อ
ที่ components/UserManagement.tsx
สิ่งที่เราจะทำ
- เพิ่ม query สำหรับการดึง user ออกมาด้วย
supabase.from('users').select('*')
- ทำการเพิ่ม searchbox และจัดการ handle search ผ่าน
.like('fullname', $searchValue)
- ทำ pagination โดยการ handle state page เพิ่มมา และทำการเรียกข้อมูลแบบ pagination ผ่าน
.range((page - 1) * itemsPerPage, page * itemsPerPage - 1)
'use client'
import { useEffect, useState } from 'react'import { createClient } from '@/utils/supabase/client'
export default function UserManagement() { const supabase = createClient() const itemsPerPage = 2
const [searchValue, setSearchValue] = useState('') const [users, setUsers] = useState<any>([]) const [page, setPage] = useState(1) const [numberOfUsers, setNumberOfUsers] = useState<number>(1)
const userSupabaseQuery = () => { let query = supabase .from('users') .select('*', { count: 'exact' }) if (searchValue) { query = query.like('fullname', `%${searchValue}%`) } query = query.range((page - 1) * itemsPerPage, page * itemsPerPage - 1) return query }
const fetchUsers = async () => { const { data: usersData, error, count } = await userSupabaseQuery() if (!usersData || error) { return false } setUsers(usersData) setNumberOfUsers(count || 1) }
useEffect(() => { fetchUsers() }, [page])
const handleSearchChange = (event: any) => { setSearchValue(event.target.value) }
const searchUser = async () => { const { data: usersData, error, count } = await userSupabaseQuery() setUsers(usersData) setPage(1) setNumberOfUsers(count || 1) }
return ( <div className="animate-in flex-1 flex flex-col gap-20 opacity-0 px-3"> <div className="flex gap-2 items-center"> <input className="rounded-md px-4 py-2 bg-inherit border w-full" type="text" onChange={handleSearchChange} /> <button onClick={searchUser}>Search</button> </div> <main className="flex-1 flex flex-col gap-6"> <table className="table-auto border-collapse border border-gray-200 w-full"> <thead> <tr> <th className="border border-gray-200 px-4 py-2">ID</th> <th className="border border-gray-200 px-4 py-2">Full Name</th> <th className="border border-gray-200 px-4 py-2">Email</th> <th className="border border-gray-200 px-4 py-2">Telephone</th> <th className="border border-gray-200 px-4 py-2">Attachment</th> </tr> </thead> <tbody> {users?.map((user) => ( <tr key={user.id}> <td>{user.id}</td> <td>{user.fullname}</td> <td>{user.email}</td> <td>{user.tel}</td> <td> <a href={user.attachment}>Download file</a> </td> </tr> ))} </tbody> </table> <div className="px-5 py-5 flex flex-col xs:flex-row items-center xs:justify-between"> <span className="text-xs xs:text-sm "> Page {page}/{numberOfUsers/itemsPerPage} of{' '} {numberOfUsers} Entries </span> <div className="inline-flex mt-2 xs:mt-0"> {page > 1 ? ( <button onClick={() => setPage(page - 1)} className="text-sm leading-none border border-solid font-bold uppercase px-3 py-1 rounded outline-none focus:outline-none mr-1 mb-1" type="button" > Previous </button> ) : ( '' )} {page < numberOfUsers / itemsPerPage ? ( <button onClick={() => setPage(page + 1)} className="text-sm leading-none border border-solid font-bold uppercase px-3 py-1 rounded outline-none focus:outline-none mr-1 mb-1" type="button" > Next </button> ) : ( '' )} </div> </div> </main> </div> )}
ผลลัพธ์ทั้งหมดเมื่อมีการเพิ่มส่วนนี้เข้าไป
- จะสามารถค้นหาข้อมูล user ได้
- จะสามารถทำ pagination ได้ สามารถเปลี่ยนไปมาระหว่างหน้าได้
ที่ middleware.ts
สุดท้ายป้องกันคนที่ไม่ได้ login เข้ามาได้ โดยการเพิ่มส่วนของ middleware เข้ามาโดย
- ทำการเพิ่มการเรียกข้อมูล user เข้ามา recheck ก่อน แต่จะเรียกเช็คเฉพาะ path
/users
เท่านั้น (เพื่อจะได้ไม่ต้องเปลืองการดึงข้อมูล user เกินความจำเป็น) - หากไม่เจอข้อมูล user = ให้ redirect กลับไปยังหน้า
/login
เพื่อให้ทำการ login ก่อนเข้ามาหน้านี้
import { NextResponse, type NextRequest } from 'next/server'import { createClient } from '@/utils/supabase/middleware'
export async function middleware(request: NextRequest) { try { const { supabase, response } = createClient(request) await supabase.auth.getSession() if (request.nextUrl.pathname.includes('users')) { const { data: { user }, } = await supabase.auth.getUser()
if (!user) { return NextResponse.redirect(new URL('/login', request.url)) } }
return response } catch (e) { return NextResponse.next({ request: { headers: request.headers, }, }) }}
export const config = { matcher: [ '/login', '/users' ],}
เทียบระหว่าง Supabase และ Firebase
และนี่ก็คือการใช้งาน Supabase กับ Table และ Storage โดยประมาณ ส่วนตัวของผม จุดที่ผมชอบจริงๆของ supabase เลยคือเรื่องของ “document ตาม usecase” ถือเป็น 1 ใน service ที่มีตัวอย่างเตรียมไว้ค่อนข้างมาก รวมถึงการเตรียมตัวอย่างคำสั่งที่ครอบคลุมด้วย ทำให้หยิบมาใช้งานได้ไม่ยากเช่นเดียวกัน
หากจะพิจารณาระหว่าง Firebase กับ Supabase สำหรับผม ผมจะพิจารณา 2 เรื่องใหญ่ๆคือ
- ต้องการ
SQL
หรือNoSQL
- ถ้ายังต้องการคุณสมบัติของ
SQL
อยู่ (ต้องการความเป็น relation อยู่) = Supabase ตอบโจทย์นี้แน่นอน - ถ้าอยากจัดการข้อมูลแบบ dynamic (ไม่ต้อง strict โครงสร้าง) = Firebase ก็จะตอบโจทย์เรื่องนี้กว่า
- Pricing
- ถ้าไม่ต้องการคำนวน Pricing ตามจำนวน Request = Supabase ตอบโจทย์กว่า
- ถ้าอยากได้ Pricing แบบคำนวนทุกจุดตามการใช้งานจริง (pay per use) = Firebase อาจจะตอบโจทย์กว่า
- Policy
- ถ้าต้องการความสามารถป้องกัน Security แบบ SQL (Row Level Security) = เลือก Supabase เลย
- ถ้ายังคงชอบการจัดการ Rule ผ่าน JSON อยู่ = เลือก Firebase เลย
ส่วนที่เหลือ อาจจะต้องพิจารณาเป็น service by service ไปว่า มีของตามที่เราต้องการครบไหม / มีจุดไหนขาดไปไหม ซึ่งส่วนตัวผมมองว่า จากที่ลองใช้มา Supabase จะค่อนข้างตอบโจทย์กับ Web application แบบทั่วไป (โดยเฉพาะ CMS) มากกว่า Firebase ถ้าเราใช้เพียง Database, Backend function และ เน้นเรื่องการดึงข้อมูล Request ถี่ๆ (ที่จะไม่ต้องกังวลเรื่อง Pricing จากการ call request)
แต่ในแง่จุดแข็งของ Firebase ก็ยังคงมีเรื่องการใช้งานร่วมกับ Google Service (เช่น Cloud run, Cloud function หรือ Cloud service อื่นๆของ Google) ดังนั้น ถ้า Service ของเรานั้นยังสะดวกต่อการใช้ Google Service มากกว่า Firebase ก็ยังคงตอบโจทย์อยู่เช่นเดียวกัน ส่งผลทำให้ทำโจทย์ได้กว้างกว่าเช่นเดียวกัน รวมถึงการจัดการแบบ realtime ที่เรียบง่ายกว่าและสามารถจัดการผ่าน JSON ได้ สำหรับ case realtime ก็ยังคง implement ง่ายกว่าฝั่ง Supabase เช่นเดียวกัน
ดังนั้นหากจะเลือกว่าใช้ Technology ตัวไหนในการทำ application ให้คำนึงตามโจทย์เป็นหลักว่า
- Firebase / Supabase ใครตอบโจทย์ได้ครบหมดหรือไม่ ?
- และ Pricing ระหว่างคิดตาม Request (Pay as you go แบบ Firebase) กับคิดแบบ Fixed (ใช้ตาม Quota จ่ายแบบเหมาแบบ Supabase) แบบไหนคำนวนได้ง่ายกว่าหรือถูกกว่า
ลองพิจารณากันตามความเหมาะสมนะครับ 😁
Github Source code
https://github.com/mikelopster/next-register-supabase
- รู้จักกับ Kafka distribution system สำหรับ Realtime กันมี Video มี Github
มาทำความรู้จัก Kafka กันว่า Kafka มันคืออะไร ใช้ทำอะไรบ้าง มี use case แบบไหน และลองมาละเลงผ่าน code กัน
- แนะนำ Dynamic programming แบบนิ่มนวลที่สุดมี Video
บทความนี้จะแนะนำเบื้องต้นเกี่ยวกับ Dynamic programming เทคนิคหนึ่งที่ใช้สำหรับแก้ปัญหาที่ ปัญหาย่อยที่ทับซ้อนกัน (overlapping subproblem)
- มาแก้ปัญหา Firestore กับปัญหาราคา Read pricing สุดจี๊ดมี Video มี Github
ในฐานะที่เป็นผู้ใช้ Firebase เหมือนกัน เรามาลองชวนคุยกันดีกว่า ว่าเราจะสามารถหาวิธีลด Pricing หรือจำนวนการ read ของ Firestore ได้ยังไงกันบ้าง
- รู้จักกับ Next.js 14 แบบ Quick Overviewมี Video มี Github
พาทัวร์ feature ต่างๆของ Next.js กันแบบรวดเร็วกัน ดูทุก feature ของ Next กัน