FileEditViewTerminal
IT Issue Bootcamp - Visual Studio Code
day-4-hour-1.mdx

Day 4 / Hour 1

Supabase Setup and Schema

Supabase project, issues table, status enum, seed data, and environment variables.

60 minutes
Supabase has an issues table and seed data.

Supabase Setup and Schema

Day 4 - ชั่วโมงที่ 1: สร้าง Database ด้วย Supabase

เป้าหมายของชั่วโมงนี้

หลังจบชั่วโมงนี้ นักศึกษาจะสามารถ:

  1. อธิบายได้ว่าทำไมโปรเจกต์ต้องเปลี่ยนจาก Mock State เป็น Database
  2. สร้าง Supabase Project สำหรับการเรียนได้
  3. สร้าง Enum และ Table issues ด้วย SQL
  4. เข้าใจ Primary Key, Default Value และ Constraint เบื้องต้น
  5. เปิด RLS และสร้าง Policy ชั่วคราวสำหรับ Demo
  6. เพิ่ม Seed Data และตรวจข้อมูลใน Table
  7. เตรียม Environment Variables ให้ Next.js พร้อมเชื่อมต่อ Supabase
  8. ปรับ 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 Database

Free Project เหมาะกับการทดลอง แต่มี Quota และอาจถูก Pause เมื่อไม่มี Activity จึงไม่ควรใช้เป็น Production โดยไม่ตรวจ Plan และข้อจำกัดก่อน


Slide 3: สร้าง Supabase Project

ทำใน Supabase Dashboard:

  1. สร้าง Project ใหม่
  2. ตั้งชื่อ เช่น it-issue-report
  3. ตั้ง Database Password ที่คาดเดายาก
  4. เลือก Region ที่ใกล้ผู้ใช้งาน
  5. ตรวจค่าใช้จ่ายหรือ Plan ก่อนกดยืนยัน
  6. รอจน 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:

TypeScriptDatabaseหน้าที่
ididรหัสของ Issue
reporterNamereporter_nameชื่อผู้แจ้ง
reporterEmailreporter_emailอีเมลผู้แจ้ง
titletitleหัวข้อปัญหา
descriptiondescriptionรายละเอียด
statusstatusสถานะงาน
adminCommentadmin_commentความเห็นผู้ดูแล
createdAtcreated_atเวลาที่สร้าง
updatedAtupdated_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 ใช้ระบุแต่ละ Row
  • default gen_random_uuid() ให้ Database สร้าง ID อัตโนมัติ
title text not null check (char_length(title) >= 5)
  • text เก็บข้อความ
  • not null ห้ามส่งค่า null
  • check บังคับให้ 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);

อ่านคำสั่งตามลำดับ:

  1. เปิดระบบ RLS ให้ Table
alter table public.issues enable row level security;

หลังเปิดแล้ว Request ต้องผ่าน Policy ที่ตรงกับคำสั่งและ Role

  1. ล้างสิทธิ์เดิม แล้วเปิดกลับมาเฉพาะงานที่ระบบต้องใช้
revoke all on public.issues from anon, authenticated;
grant select, insert, update
  on public.issues
  to anon, authenticated;
  • anon คือ Request ที่ยังไม่ Login
  • authenticated คือ Request ของผู้ที่ Login แล้ว
  • ไม่ให้ delete เพราะระบบใช้ Status DONE แทนการลบ
  1. สร้าง Policy แยกตามคำสั่ง
  • for select ใช้ using (true) เพื่อให้อ่านทุก Row
  • for 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 ที่ใช้ Policy
  • using (...) ตรวจ Row ที่อ่านหรือ Row เดิมก่อน Update
  • with check (...) ตรวจ Row ใหม่ก่อน Insert หรือหลัง Update
  • true หมายถึงทุก 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.example Commit ได้เพราะไม่มี 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 ใน Git
  • Issue มี updatedAt
  • IssueForm.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


คำศัพท์สำคัญ

คำศัพท์ความหมาย
SupabasePlatform ที่มี Postgres, Data API, Auth และเครื่องมือจัดการ
Schemaกลุ่มที่ใช้จัดระเบียบ Object ใน Database เช่น public
Primary KeyColumn ที่ระบุแต่ละ Row แบบไม่ซ้ำ
EnumType ที่รับได้เฉพาะค่าที่กำหนด
Constraintกติกาที่ Database ตรวจข้อมูลก่อนบันทึก
RLSระบบ Policy ที่ควบคุมสิทธิ์ระดับ Row
Seed Dataข้อมูลตัวอย่างเริ่มต้นสำหรับทดลองระบบ
Environment Variableค่าตั้งค่าที่แยกออกจาก Source Code