Supabase Setup and Schema
Day 4 - ชั่วโมงที่ 1: สร้าง Database ด้วย Supabase
เป้าหมายของชั่วโมงนี้
หลังจบชั่วโมงนี้ นักศึกษาจะสามารถ:
- อธิบายได้ว่าทำไมโปรเจกต์ต้องเปลี่ยนจาก Mock State เป็น Database
- สร้าง Supabase Project สำหรับการเรียนได้
- สร้าง Enum และ Table
issuesด้วย SQL - เข้าใจ Primary Key, Default Value และ Constraint เบื้องต้น
- เปิด RLS และสร้าง Policy ชั่วคราวสำหรับ Demo
- เพิ่ม Seed Data และตรวจข้อมูลใน Table
- เตรียม Environment Variables ให้ Next.js พร้อมเชื่อมต่อ Supabase
- ปรับ TypeScript Type ให้ตรงกับ Database
สิ่งที่ใช้ในชั่วโมงนี้
ทำงานหลักใน:
Supabase Dashboard
└── SQL Editorไฟล์ที่สร้างหรือแก้ใน Project:
.env.local
.env.example
.gitignore
types/issue.ts
components/IssueForm.tsx
data/issue.tsโครงสร้างเวลา 60 นาที
| เวลา | เนื้อหา |
|---|---|
| 0-10 นาที | จาก Mock State ไป Database |
| 10-20 นาที | สร้าง Supabase Project |
| 20-40 นาที | สร้าง Enum, Table และ Constraint |
| 40-50 นาที | เปิด RLS และ Demo Policies |
| 50-60 นาที | Seed Data, Environment Variables และ TypeScript Type |
Slide 1: Recap จาก Day 3
Day 3 เราสร้าง Frontend Prototype ที่ทำงานได้แล้ว:
- สร้าง Issue จาก Form
- ตรวจข้อมูลและแสดง Error
- แสดง Issue ใน List
- เปลี่ยน Status ใน State
แต่ข้อมูลยังอยู่ใน Memory ของ Browser:
Refresh หน้า → State เริ่มใหม่ → ข้อมูลที่เพิ่มและแก้ไขหายDay 4 เราจะเปลี่ยนแหล่งข้อมูลหลัก:
Day 3: useState เป็น Source of Truth
Day 4: Database เป็น Source of Truthเมื่อข้อมูลอยู่ใน Database ทุกหน้าและทุก Browser จะอ่านข้อมูลชุดเดียวกันได้
Slide 2: Supabase ช่วยอะไรเรา
Supabase เป็น Platform ที่มี Postgres Database และเครื่องมือสำหรับเชื่อม Application เข้ากับข้อมูล
สิ่งที่เราจะใช้ในหลักสูตร:
- Postgres สำหรับเก็บ Issue
- Table Editor สำหรับดูข้อมูล
- SQL Editor สำหรับสร้าง Table และ Policy
- Data API สำหรับให้ Next.js อ่านและแก้ข้อมูล
- Auth ใน Day 5 สำหรับ Login และกำหนดสิทธิ์
Flow ใหม่ของระบบ:
Next.js
↓ Supabase Client
Data API
↓ ตรวจ GRANT และ RLS
Postgres DatabaseFree Project เหมาะกับการทดลอง แต่มี Quota และอาจถูก Pause เมื่อไม่มี Activity จึงไม่ควรใช้เป็น Production โดยไม่ตรวจ Plan และข้อจำกัดก่อน
Slide 3: สร้าง Supabase Project
ทำใน Supabase Dashboard:
- สร้าง Project ใหม่
- ตั้งชื่อ เช่น
it-issue-report - ตั้ง Database Password ที่คาดเดายาก
- เลือก Region ที่ใกล้ผู้ใช้งาน
- ตรวจค่าใช้จ่ายหรือ Plan ก่อนกดยืนยัน
- รอจน Project พร้อมใช้งาน
ตั้งค่า Data API อย่างปลอดภัย
ถ้าหน้าสร้าง Project แสดงตัวเลือกเหล่านี้:
- เปิด Enable Data API
- ปิด Automatically expose new tables and functions
- เปิด Enable automatic RLS
การปิดการเปิดเผย Table อัตโนมัติทำให้ Table ใหม่ยังไม่เรียกผ่าน Data API จนกว่าเราจะ GRANT สิทธิ์ที่จำเป็นใน Slide 9
เก็บ Database Password ไว้ใน Password Manager และห้ามใส่ Password, Secret Key หรือ Service Role Key ลง GitHub
Slide 4: ออกแบบข้อมูลก่อนเขียน SQL
TypeScript ใน Day 3 ใช้ชื่อแบบ camelCase แต่ Database จะใช้ snake_case:
| TypeScript | Database | หน้าที่ |
|---|---|---|
id | id | รหัสของ Issue |
reporterName | reporter_name | ชื่อผู้แจ้ง |
reporterEmail | reporter_email | อีเมลผู้แจ้ง |
title | title | หัวข้อปัญหา |
description | description | รายละเอียด |
status | status | สถานะงาน |
adminComment | admin_comment | ความเห็นผู้ดูแล |
createdAt | created_at | เวลาที่สร้าง |
updatedAt | updated_at | เวลาที่แก้ล่าสุด |
Hour 2 จะสร้าง Function สำหรับแปลงชื่อ Column จาก Database Row ให้เป็น TypeScript Object ที่ Component ใช้อยู่
Slide 5: สร้าง Enum สำหรับ Status
เปิด Supabase SQL Editor แล้วรันคำสั่งนี้หนึ่งครั้ง:
create type public.issue_status
as enum ('OPEN', 'IN_PROGRESS', 'DONE');Enum คือชนิดข้อมูลที่รับได้เฉพาะค่าที่กำหนด:
OPEN
IN_PROGRESS
DONEแนวคิดเหมือน Union Type ที่เราใช้ใน TypeScript:
type IssueStatus = "OPEN" | "IN_PROGRESS" | "DONE";ต้องสร้าง issue_status ก่อน Table เพราะ Column status ใน Slide ถัดไปจะอ้างถึง Type นี้
ถ้ารันคำสั่งเดิมซ้ำ Database จะแจ้งว่าชื่อนี้มีอยู่แล้ว เมื่อรันสำเร็จให้ไป Slide ถัดไปโดยไม่ต้องรันซ้ำ
Slide 6: สร้าง Table issues
รันต่อใน Supabase SQL Editor:
create table public.issues (
id uuid primary key default gen_random_uuid(),
reporter_name text not null,
reporter_email text not null,
title text not null check (char_length(title) >= 5),
description text not null check (char_length(description) >= 10),
status public.issue_status not null default 'OPEN',
admin_comment text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);public.issues ระบุชัดว่า Table ชื่อ issues อยู่ใน Schema public ซึ่งเป็น Schema ที่เราจะใช้งานผ่าน Data API
หลังรันสำเร็จ เปิด Table Editor แล้วควรเห็น Table issues พร้อม 9 Columns
อ่าน SQL ของ Table ทีละส่วน
id uuid primary key default gen_random_uuid()uuidเก็บรหัสที่ไม่ซ้ำกันprimary keyใช้ระบุแต่ละ Rowdefault gen_random_uuid()ให้ Database สร้าง ID อัตโนมัติ
title text not null check (char_length(title) >= 5)textเก็บข้อความnot nullห้ามส่งค่าnullcheckบังคับให้ Title ยาวอย่างน้อย 5 ตัวอักษร
status public.issue_status not null default 'OPEN'- รับเฉพาะค่าจาก Enum
issue_status - ถ้าไม่ได้ส่ง Status มา จะเริ่มเป็น
OPEN
created_at timestamptz not null default now()timestamptz เก็บวันเวลาแบบมี Time Zone และ now() ใส่เวลาปัจจุบันให้อัตโนมัติ
Slide 7: กติกาใน Table ช่วยป้องกันข้อมูลผิด
ใน Slide 6 เราไม่ได้กำหนดแค่ชื่อและ Type ของ Column แต่ยังใส่กติกาเหล่านี้ด้วย:
primary key
not null
check (...)กติกาที่ Database ใช้ควบคุมข้อมูลเหล่านี้เรียกรวมว่า Constraint
Constraint คือกติกาที่ Database ตรวจทุกครั้งก่อนบันทึกข้อมูล
Frontend Validation
↓ ช่วยให้ผู้ใช้แก้ข้อมูลเร็ว
Server Validation
↓ ไม่เชื่อข้อมูลจาก Client
Database Constraint
↓ ป้องกันข้อมูลผิดเข้า Table
บันทึกข้อมูลตัวอย่าง:
description text not null
check (char_length(description) >= 10)แม้มีคนส่ง Request โดยไม่ผ่าน Form ถ้ารายละเอียดยาวไม่ถึง 10 ตัวอักษร Database จะปฏิเสธข้อมูล
Constraint ไม่ได้แทน Server Validation แต่เป็นด่านสุดท้ายที่ช่วยรักษาคุณภาพข้อมูลใน Table
Slide 8: เปิด RLS และให้สิทธิ์สำหรับ Demo
RLS คืออะไร
Row Level Security หรือ RLS คือระบบ Policy ที่ตรวจว่า Request มีสิทธิ์ทำงานกับ Row ใดบ้าง
ใน Day 4 เรายังไม่มี Login จึงเปิดสิทธิ์ชั่วคราวให้ทั้ง Request ที่ยังไม่ Login (anon) และ Request ที่ Login แล้ว (authenticated) เพื่อไม่ให้ Flow ขาดก่อนปรับเป็น Policy จริงใน Day 5
รัน SQL นี้หลังสร้าง Table:
alter table public.issues enable row level security;
revoke all on public.issues from anon, authenticated;
grant select, insert, update
on public.issues
to anon, authenticated;
create policy "demo_select_issues"
on public.issues
for select
to anon, authenticated
using (true);
create policy "demo_insert_issues"
on public.issues
for insert
to anon, authenticated
with check (true);
create policy "demo_update_issues"
on public.issues
for update
to anon, authenticated
using (true)
with check (true);อ่านคำสั่งตามลำดับ:
- เปิดระบบ RLS ให้ Table
alter table public.issues enable row level security;หลังเปิดแล้ว Request ต้องผ่าน Policy ที่ตรงกับคำสั่งและ Role
- ล้างสิทธิ์เดิม แล้วเปิดกลับมาเฉพาะงานที่ระบบต้องใช้
revoke all on public.issues from anon, authenticated;
grant select, insert, update
on public.issues
to anon, authenticated;anonคือ Request ที่ยังไม่ Loginauthenticatedคือ Request ของผู้ที่ Login แล้ว- ไม่ให้
deleteเพราะระบบใช้ StatusDONEแทนการลบ
- สร้าง Policy แยกตามคำสั่ง
for selectใช้using (true)เพื่อให้อ่านทุก Rowfor insertใช้with check (true)เพื่อให้เพิ่ม Row ได้for updateใช้ทั้งusing (true)ตรวจ Row เดิม และwith check (true)ตรวจ Row หลังแก้
Policy ชุดนี้ใช้เฉพาะ Project ฝึก เพราะทุก Row ผ่านเงื่อนไข true
Slide 9: GRANT และ Policy ทำงานคนละชั้น
การเรียก Table ผ่าน Data API ต้องผ่านสองชั้น:
ชั้นที่ 1: GRANT
Role ทำคำสั่ง SELECT / INSERT / UPDATE ได้หรือไม่
↓
ชั้นที่ 2: RLS Policy
Role ทำคำสั่งนั้นกับ Row ใดได้บ้างคำสำคัญใน Policy:
to anon, authenticatedระบุ Role ที่ใช้ Policyusing (...)ตรวจ Row ที่อ่านหรือ Row เดิมก่อน Updatewith check (...)ตรวจ Row ใหม่ก่อน Insert หรือหลัง Updatetrueหมายถึงทุก Row ผ่านเงื่อนไข
ใน Day 5 เราจะลบ Policy demo_*, ถอนสิทธิ์ของ anon และเปลี่ยนเงื่อนไขให้ผูกกับผู้ใช้ที่ Login และ Role ของผู้ใช้
อย่าใช้ Policy using (true) และ with check (true) กับข้อมูลจริงที่ต้องจำกัดสิทธิ์
Slide 10: เพิ่มและตรวจ Seed Data
Seed Data คือข้อมูลตัวอย่างสำหรับทดลอง Query และตรวจว่า UI แสดงผลได้
รันใน SQL Editor:
insert into public.issues
(reporter_name, reporter_email, title, description, status, admin_comment)
values
(
'Anan',
'anan@example.com',
'Login เข้าระบบไม่ได้',
'ไม่สามารถเข้าสู่ระบบด้วยบัญชีเดิมได้',
'OPEN',
null
),
(
'Mali',
'mali@example.com',
'ส่งแบบฟอร์มสมัครไม่ได้',
'กดส่งข้อมูลแล้วระบบขึ้น error',
'IN_PROGRESS',
null
),
(
'Kanda',
'kanda@example.com',
'ขอสิทธิ์เข้าใช้งาน dashboard',
'ต้องการสิทธิ์สำหรับตรวจสอบข้อมูลหลังบ้าน',
'DONE',
'อนุมัติสิทธิ์และแจ้งผู้ใช้เรียบร้อยแล้ว'
);ตรวจผลด้วย Table Editor หรือรัน:
select id, reporter_name, title, status, created_at
from public.issues
order by created_at desc;ควรได้ 3 Rows และแต่ละ Row มี UUID กับเวลา created_at ที่ Database สร้างให้
Slide 11: เตรียม Environment Variables
สร้าง .env.local ที่ Project Root ระดับเดียวกับ package.json:
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_...หา Project URL และ Publishable Key จากปุ่ม Connect ของ Project หรือหน้า Settings → API Keys
จากนั้นสร้าง .env.example ที่ Project Root เพื่อบอกชื่อค่าที่ Project ต้องใช้ โดยไม่ใส่ค่าจริง:
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=Create Next App ใช้ .env* ใน .gitignore ซึ่งจะ Ignore .env.example ไปด้วย ให้เพิ่มบรรทัดยกเว้นต่อจาก .env*:
.env*
!.env.exampleเครื่องหมาย ! หมายถึงไม่ต้อง Ignore .env.example ทำให้ไฟล์ตัวอย่างถูก Commit ได้ แต่ .env.local ยังถูก Ignore เหมือนเดิม
.env.localใช้รัน Project จริงและถูก.gitignoreไว้.env.exampleCommit ได้เพราะไม่มี Key จริงNEXT_PUBLIC_หมายถึงค่าอาจถูกส่งไป Browser จึงใช้ได้เฉพาะ URL และ Publishable Key- ห้ามใส่ Secret Key หรือ Service Role Key ในตัวแปรที่ขึ้นต้นด้วย
NEXT_PUBLIC_
หลังแก้ .env.local ให้ Restart Development Server เพื่อให้ Next.js โหลดค่าใหม่
Slide 12: ปรับ TypeScript Type ให้ตรงกับ Table
แก้บางส่วนใน types/issue.ts โดยเพิ่ม updatedAt ต่อจาก createdAt:
export type IssueStatus = "OPEN" | "IN_PROGRESS" | "DONE";
export type Issue = {
id: string;
reporterName: string;
reporterEmail: string;
title: string;
description: string;
status: IssueStatus;
adminComment?: string;
createdAt: string;
updatedAt: string;
};บรรทัดที่ Highlight คือ Field ใหม่ที่เพิ่มให้ตรงกับ Column updated_at ใน Database:
updatedAt: string;เมื่อเพิ่ม Field ที่บังคับ TypeScript จะช่วยหา Object เดิมที่ยังไม่มี updatedAt ใน IssueForm.tsx และ data/issue.ts
Slide 13: แก้ Object ที่สร้าง Issue ให้ครบ
แก้ components/IssueForm.tsx
แทน Function createIssueFormInput() เดิมด้วย:
function createIssueFormInput(input: NewIssueInput): Issue {
const now = new Date().toISOString();
return {
id: crypto.randomUUID().slice(0, 8),
reporterName: input.reporterName,
reporterEmail: input.reporterEmail,
title: input.title,
description: input.description,
status: "OPEN",
createdAt: now,
updatedAt: now,
};
}ตอนสร้างใหม่ createdAt และ updatedAt ใช้เวลาเดียวกัน
แก้ data/issue.ts
เพิ่ม updatedAt ให้ทุก Object ต่อจาก createdAt:
createdAt: "2026-05-08",
updatedAt: "2026-05-08",ต้องแก้ทุก Issue ใน Array เพราะทั้งหมดประกาศเป็น Issue[]
Slide 14: ตรวจความพร้อมก่อนเข้า Hour 2
Supabase Dashboard
- Project พร้อมใช้งาน
- มี Enum
issue_status - มี Table
public.issuesจำนวน 9 Columns - RLS เปิดอยู่
- มี Demo Policies สำหรับ Select, Insert และ Update
- Query เห็น Seed Data 3 Rows
Next.js Project
.env.localมี Project URL และ Publishable Key.env.exampleมีเฉพาะชื่อ Environment Variables.gitignoreมี!.env.example.env.localไม่ถูก Stage ใน GitIssueมีupdatedAtIssueForm.tsxและ Mock Data ไม่มี TypeScript Error
ตอนนี้ยังไม่ต้องลบ IssueBoard หรือ data/issue.ts เพราะ Hour 2 จะค่อยเปลี่ยนหน้า /issues จาก Mock Data ไปอ่าน Supabase ทีละขั้น
Slide 15: สรุป Hour 1
สิ่งที่ได้เรียน:
- Database ทำให้ข้อมูลอยู่ถาวรกว่า State ใน Browser
- Supabase ใช้ Postgres และเปิดให้ Next.js ติดต่อผ่าน Data API
- Enum จำกัดค่าของ Status
- Constraint ป้องกันข้อมูลผิดก่อนเข้า Table
GRANTให้สิทธิ์ระดับ Table ส่วน RLS Policy ควบคุมระดับ Row- Demo Policy ใช้ชั่วคราวและต้องเปลี่ยนก่อนใช้ระบบจริง
- Environment Variables แยกค่าตั้งค่าออกจาก Source Code
- TypeScript Type และ Database Schema ต้องมี Field สอดคล้องกัน
Hour ถัดไป เราจะติดตั้ง Supabase Client และเปลี่ยนหน้า /issues ให้ Query ข้อมูลจริงจาก Database
คำศัพท์สำคัญ
| คำศัพท์ | ความหมาย |
|---|---|
| Supabase | Platform ที่มี Postgres, Data API, Auth และเครื่องมือจัดการ |
| Schema | กลุ่มที่ใช้จัดระเบียบ Object ใน Database เช่น public |
| Primary Key | Column ที่ระบุแต่ละ Row แบบไม่ซ้ำ |
| Enum | Type ที่รับได้เฉพาะค่าที่กำหนด |
| Constraint | กติกาที่ Database ตรวจข้อมูลก่อนบันทึก |
| RLS | ระบบ Policy ที่ควบคุมสิทธิ์ระดับ Row |
| Seed Data | ข้อมูลตัวอย่างเริ่มต้นสำหรับทดลองระบบ |
| Environment Variable | ค่าตั้งค่าที่แยกออกจาก Source Code |