ลองเล่น Supabase กับ Next.js กัน

/ 12 min read

Share on social media

supabase-next สามารถดู video ของหัวข้อนี้ก่อนได้ ดู video

Supabase คืออะไร ?

Ref: https://supabase.com/docs

supabase-intro

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 คือ

  1. Database ใช้ PostgreSQL ซึ่งถือว่าเป็น DB สุดทรงพลังที่สามารถ query ข้อมูลได้หลากหลายประเภท รวมถึงสามารถทำ complex queries, relation (ตามคุณสมบัติของ SQL) ได้
  2. มี Authentication service ที่สามารถทำ Service Auth เอง หรือจะใช้ร่วมกับ 3rd party อย่าง Google, GitHub, Facebook, X (Twitter) ก็ได้
  3. มี Feature realtime ที่สามารถดึงข้อมูลแบบ realtime เมื่อมี change กับ data เกิดขึ้นได้
  4. มี Storage ที่สามารถเก็บไฟล์ขนาดใหญ่เช่น ภาพ, video หรือไฟล์ประเภทต่างๆที่อนุญาตให้ user upload เข้ามาได้
  5. มี API ที่ supabase ได้ทำการ generate ออกมาเพื่อให้สามารถต่อไปยัง database เพื่อให้สามารถจัดการ CRUD (Create, Read, Update, Delete) กับ Database ได้ง่ายขึ้น
  6. มี Edge Functions ที่ทำให้ supabase สามารถ run code แบบ server-side code ออกมาได้
  7. มี Dashboard สามารถจัดการกับทุก Service ของ supabase ได้ (รวมถึงสามารถใช้คำสั่ง query ใน Dashboard ได้)

จุดเด่นจริงๆเลยของ Supabase คือ “เชื่อมง่าย” และ “ตัวอย่างเยอะ” เนื่องจากเอกสารแทบจะเตรียมวิธีไว้ให้หมดแล้วว่าวิธีเชื่อมแต่ละอย่าง วิธีใช้งานแต่ละอันสามารถทำอย่างไรได้บ้าง เดี๋ยวเรามาเรียนรู้ไปพร้อมๆกันในบทความนี้

และอีกจุดหนึ่ง (ซึ่งเป็นจุดชูโรงที่ Supabase เทียบกับ Firebase ด้วย) คือการ “ไม่คิด pricing ตาม request”

supabase-pricing

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 ฝั่งคือ

  1. ฝั่งหน้าบ้าน (สำหรับผู้ใช้ทั่วไป)
  • สามารถกรอกฟอร์มเพื่อ submit ข้อมูลเข้ามาได้ โดยข้อมูลจะประกอบด้วย fullname (ชื่อ), email และ tel (เบอร์โทรศัพท์)
  • สามารถแนบ upload ไฟล์มาได้ (จะนำไปเก็บไว้ใน supabase storage)

ให้อารมณ์เหมือนทำหน้าเว็บพร้อมกรอกใบสมัครพร้อมเอกสารเข้ามา

  1. ฝั่งหลังบ้าน (สำหรับ user ที่ login)
  • แสดง user ทั้งหมดที่ทำการกรอกเข้าระบบมา สามารถดูไฟล์แนบเขาได้
  • ทำ Search ได้ รวมถึงสามารถทำ pagination ได้

เราจะทำตัวอย่างประมาณนี้กัน let’s code ทุกคน

เริ่มต้น code กัน

เริ่มต้น project ตาม guideline ของ supabase เลย โดยการ run ผ่าน command

Terminal window
npx create-next-app -e with-supabase

หลังจากนั้น หยิบ key จาก dashboard วางใน .env.local ตามใน Dashboard ของ supabase

NEXT_PUBLIC_SUPABASE_URL=your-project-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key

โดยมันจะอยู่ในส่วนของ Project Setting > API จะมีแสดงส่วน key ออกมาอยู่

supabase-02

ให้นำ key ตรงตำแหน่ง ANON มาใส่ NEXT_PUBLIC_SUPABASE_ANON_KEY และ URL มาใส่ NEXT_PUBLIC_SUPABASE_URL

เสร็จแล้ว ลอง run project ดูด้วยคำสั่ง

Terminal window
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 ก่อนคือ

  1. ต้องสร้าง table สำหรับเก็บข้อมูล user เอาไว้ก่อน (ซึ่งเราจะตั้งชื่อว่า users)
  2. ต้องสร้าง storage สำหรับเก็บไฟล์ที่ user upload เข้ามา (ซึ่งเราจะตั้งชื่อว่า attachments)

โดยทั้ง 2 อย่างนี้สามารถสร้างได้ผ่าน dashboard supabase ได้เลย

สร้าง table

สามารถเพิ่ม table ได้จาก Table Editor

supabase-schema-01

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

supabase-schema-01

หรือ 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

supabase-schema-01

สร้าง storage

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

supabase-schema-04

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

supabase-schema-05

กำหนด 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 ได้

supabase-table-policy-01

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

supabase-table-policy-02

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

  1. SELECT สำหรับ user ที่ authenticated
supabase-table-policy-03
  1. INSERT สำหรับ user แบบ public ทั่วไป
supabase-table-policy-04

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

supabase-table-policy-05

ใส่ Policy storage attachments

โจทย์ของ Storage attachment คือ

  • มีแค่เฉพาะ คนที่ login เท่านั้น ถึงจะสามารถดูข้อมูลไฟล์ที่ upload เข้ามาได้
  • บุคคลทั่วไป สามารถส่งข้อมูลเข้ามาเพิ่มได้ แต่ แก้ไข, ลบ รวมถึงกลับมาดูไม่ได้

โดย Storage นั้นจะสามารถเพิิ่มได้จากหัวข้อ Storage > Policies

supabase-storage-policy-01

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

  1. SELECT สำหรับ user ที่ authenticated
supabase-storage-policy-02
  1. INSERT สำหรับ user แบบ public ทั่วไป
supabase-storage-policy-03

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

supabase-storage-policy-04

เพียงเท่านี้ ทั้ง Table และ Storage เราก็จะพร้อมสำหรับการเริ่มงานเป็นที่เรียบร้อย step ต่อไปเราจะมาเริ่ม code กัน

1. ฝั่งหน้าบ้าน

ไฟล์ที่เกี่ยวข้อง

.
├── app
│ ├── page.tsx --> หน้าหลัก
│ ├── actions.ts --> ส่วนสำหรับรับ register

ตามที่เราอธิบายไว้ตอนแรก โจทย์หลักของฝั่งหน้าบ้านจะมี 2 โจทย์คือ

  1. สามารถกรอกฟอร์มเพื่อ submit ข้อมูลเข้ามาได้ โดยข้อมูลจะประกอบด้วย fullname (ชื่อ), email และ tel (เบอร์โทรศัพท์)
  2. สามารถแนบ upload ไฟล์มาได้ (จะนำไปเก็บไว้ใน supabase storage)

ผลลัพธ์จะได้ออกมาประมาณนี้

supabase-example

เราจะเริ่มแก้ไขที่ละไฟล์กัน

ที่ page.tsx

สิ่งที่เราจะเพิ่ม

  1. ทำการสร้าง Form สำหรับกรอก fullname, email, tel ขึ้นมา
  2. ปรับ component นี้เป็น Client component (เพื่อให้สามารถ handlr state ของ error message ด้วยคำสั่งของ React ได้)
  3. ทำการ import register() ซึ่งจะเป็น Server action ที่ทำการต่อไปยัง server ตอน submit form
  4. ทำการเชื่อม 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

สิ่งที่เราจะเพิ่ม

  1. ทำการเพิ่ม function Server action register() เข้ามาสำหรับรับค่าจาก Form ของฝั่ง Client component
  2. ทำการอ่านค่า fullname, email, tel, attachment ผ่าน formData เข้ามา
  3. อ่านค่าจาก attachment และดำเนินการ upload เข้า storage ก่อน โดยจะทำการเปลี่ยนชื่อไฟล์ที่ generate ใหม่ด้วย uuid() เพื่อให้ชื่อไฟล์ไม่ซ้ำกัน ผ่านคำสั่ง supabase.storage.from('attachments').upload(uuidFileName, attachment)
  4. หลังจาก upload เสร็จให้เอา public path มา (เพื่อเก็บไว้ใน database) ผ่านคำสั่ง supabase.storage.from('attachments').getPublicUrl(uuidFileName)
  5. นำข้อมูลทั้งหมดมาประกอบกัน โดยทำการ insert ข้อมูลเข้า table users ผ่านคำสั่ง supabase.from('users').insert()
  6. เมื่อเรียบร้อยให้ส่ง 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

ผลลัพธ์จะออกมาเป็นแบบนี้ supabase-05

ที่ 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"
placeholder="you@example.com"
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

สิ่งที่เราจะทำในหน้านี้คือ

  1. ทำการเพิ่มการเรียก <AuthButton /> (ส่วนที่มีการเชื่อม login เอาไว้ที่ CLI ทำการ generate เอาไว้) เพื่อใช้สำหรับการ handle state login (ในกรณีที่ยังไม่ login จะได้สามารถกลับไปยังหน้า login ได้)
  2. เรียกใช้ <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

สิ่งที่เราจะทำ

  1. เพิ่ม query สำหรับการดึง user ออกมาด้วย supabase.from('users').select('*')
  2. ทำการเพิ่ม searchbox และจัดการ handle search ผ่าน .like('fullname', $searchValue)
  3. ทำ 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>
)
}

ผลลัพธ์ทั้งหมดเมื่อมีการเพิ่มส่วนนี้เข้าไป

  1. จะสามารถค้นหาข้อมูล user ได้
  2. จะสามารถทำ pagination ได้ สามารถเปลี่ยนไปมาระหว่างหน้าได้ supabase-example

ที่ middleware.ts

สุดท้ายป้องกันคนที่ไม่ได้ login เข้ามาได้ โดยการเพิ่มส่วนของ middleware เข้ามาโดย

  1. ทำการเพิ่มการเรียกข้อมูล user เข้ามา recheck ก่อน แต่จะเรียกเช็คเฉพาะ path /users เท่านั้น (เพื่อจะได้ไม่ต้องเปลืองการดึงข้อมูล user เกินความจำเป็น)
  2. หากไม่เจอข้อมูล 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 เรื่องใหญ่ๆคือ

  1. ต้องการ SQL หรือ NoSQL
  • ถ้ายังต้องการคุณสมบัติของ SQL อยู่ (ต้องการความเป็น relation อยู่) = Supabase ตอบโจทย์นี้แน่นอน
  • ถ้าอยากจัดการข้อมูลแบบ dynamic (ไม่ต้อง strict โครงสร้าง) = Firebase ก็จะตอบโจทย์เรื่องนี้กว่า
  1. Pricing
  • ถ้าไม่ต้องการคำนวน Pricing ตามจำนวน Request = Supabase ตอบโจทย์กว่า
  • ถ้าอยากได้ Pricing แบบคำนวนทุกจุดตามการใช้งานจริง (pay per use) = Firebase อาจจะตอบโจทย์กว่า
  1. 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

Related Post

Share on social media