Skip to main content
Back to Blog
SupabaseRLSPostgreSQLSecurityThaiFlutterNext.js

Supabase RLS คืออะไร? ทำความเข้าใจ Row Level Security ฉบับนักพัฒนาไทย

CoreCodery Team

Get new tutorials every Tuesday

Join developers reading CoreCodery Weekly — Flutter, Cloudflare & Supabase.

No spam. Unsubscribe anytime.

Supabase RLS คืออะไร? ทำความเข้าใจ Row Level Security ฉบับนักพัฒนาไทย

Series: Thai Tech Community (Pillar 5) | Keyword: supabase rls thai tutorial | ความยาว: ~1,800 คำ


บทนำ

ถ้าคุณเคยสร้างแอปด้วย Supabase แล้วกังวลว่า "user คนหนึ่งจะอ่านข้อมูลของ user อื่นได้ไหม?" — นั่นแหละคือปัญหาที่ Row Level Security (RLS) แก้ได้

RLS คือระบบรักษาความปลอดภัยระดับ database ที่ให้คุณกำหนดได้ว่า "row ไหนที่ user คนนี้มีสิทธิ์อ่าน/เขียน/แก้ไข" โดยตรงบน PostgreSQL — ไม่ต้องเขียน backend logic เพิ่ม ไม่ต้องกลัวว่า client จะ bypass API แล้วดึงข้อมูลคนอื่นออกไป

บทความนี้ใช้ตัวอย่างจากโปรเจกต์จริง: `profiles` table ที่ใช้ร่วมกันระหว่าง CoreCodery.com (Next.js) และ Pill Timer (Flutter) บน Supabase instance เดียวกัน


RLS คืออะไร และทำไมถึงสำคัญ?

โดยปกติ เมื่อคุณสร้าง table ใน Supabase แล้วใช้ Supabase client ดึงข้อมูล — ทุกคนสามารถอ่านทุก row ได้ ถ้า table นั้น allow public access

-- ไม่มี RLS: ใครก็ดึงข้อมูลได้ทั้งหมด
SELECT * FROM profiles;  -- คืน profiles ของทุก user!

RLS แก้ปัญหานี้ โดยให้คุณเขียน "policy" — กฎที่บอกว่า SQL statement นั้นจะทำงานกับ row ไหนได้บ้าง กฎเหล่านี้ run ที่ database level ทุกครั้ง ไม่ว่า request จะมาจากทางไหนก็ตาม


Enable RLS บน Table

ก่อนสร้าง policy ใดๆ ต้อง enable RLS บน table ก่อน:

ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;

สำคัญ: เมื่อ enable RLS แล้ว แต่ยังไม่มี policy — ไม่มีใครเข้าถึงได้เลย (default deny all) นี่คือความปลอดภัยแบบ fail-safe


Policy คืออะไร และมีกี่ประเภท?

Policy คือกฎที่บอกว่า "ใคร ทำอะไร กับ row ไหน ได้บ้าง"

syntax พื้นฐาน:

CREATE POLICY "policy_name"
  ON table_name
  FOR [SELECT | INSERT | UPDATE | DELETE | ALL]
  [TO role_name]
  USING (condition)           -- ใช้กับ SELECT, UPDATE, DELETE
  WITH CHECK (condition);     -- ใช้กับ INSERT, UPDATE

USING vs WITH CHECK

  • `USING`: กรอง rows ที่มีอยู่แล้วใน database (ใช้กับ SELECT, UPDATE, DELETE)
  • `WITH CHECK`: ตรวจสอบ rows ที่กำลังเขียนเข้า database (ใช้กับ INSERT, UPDATE)

ตัวอย่าง 4 ประเภท

-- SELECT: อ่านได้เฉพาะ profile ของตัวเอง
CREATE POLICY "Users can read own profile"
  ON profiles FOR SELECT
  USING (auth.uid() = id);

-- INSERT: เพิ่มได้เฉพาะ profile ของตัวเอง
CREATE POLICY "Users can insert own profile"
  ON profiles FOR INSERT
  WITH CHECK (auth.uid() = id);

-- UPDATE: แก้ไขได้เฉพาะ profile ของตัวเอง
CREATE POLICY "Users can update own profile"
  ON profiles FOR UPDATE
  USING (auth.uid() = id);

-- DELETE: ลบได้เฉพาะ profile ของตัวเอง
CREATE POLICY "Users can delete own profile"
  ON profiles FOR DELETE
  USING (auth.uid() = id);

auth.uid() และ auth.role() คืออะไร?

Supabase มี built-in functions ที่ดึงข้อมูล session ของ user ปัจจุบัน:

auth.uid()

คืน UUID ของ user ที่ authenticated อยู่ตอนนี้ ถ้าไม่มีการ authenticate จะคืน `null`

SELECT auth.uid();  -- '550e8400-e29b-41d4-a716-446655440000'

auth.role()

คืน role ของ user ปัจจุบัน — `anon` (ยังไม่ login), `authenticated` (login แล้ว), หรือ `service_role` (admin, bypass ทุก RLS)

-- อนุญาตให้เฉพาะ authenticated user ทำ INSERT
CREATE POLICY "Authenticated users only"
  ON posts FOR INSERT
  TO authenticated
  WITH CHECK (true);

ตัวอย่างจริง: profiles Table ของ CoreCodery + Pill Timer

นี่คือ `profiles` table ที่ใช้งานจริงใน production:

CREATE TABLE profiles (
  id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
  username TEXT UNIQUE NOT NULL,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;

Policy ที่ใช้งานจริง

-- ทุกคนอ่าน username ได้ (สำหรับ leaderboard / public profile)
CREATE POLICY "Public can read profiles"
  ON profiles FOR SELECT
  USING (true);

-- เพิ่ม profile ของตัวเองเท่านั้น
CREATE POLICY "Users can insert own profile"
  ON profiles FOR INSERT
  WITH CHECK (auth.uid() = id);

-- แก้ไข profile ของตัวเองเท่านั้น
CREATE POLICY "Users can update own profile"
  ON profiles FOR UPDATE
  USING (auth.uid() = id);

ทำไมถึงให้ทุกคน SELECT ได้?

เพราะ `username` เป็นข้อมูล public — ใช้สำหรับแสดงใน leaderboard และ public profile ของ Pill Timer ถ้าใช้ `auth.uid() = id` สำหรับ SELECT ด้วย app จะแสดง username ของ user อื่นไม่ได้

การใช้งานจาก Flutter (Pill Timer)

// เพิ่ม profile ใหม่
await supabase.from('profiles').insert({
  'id': supabase.auth.currentUser!.id,
  'username': username,
});

// อ่าน profile ของ user อื่น (public policy อนุญาต)
final result = await supabase
  .from('profiles')
  .select('username')
  .eq('id', targetUserId)
  .single();

การใช้งานจาก Next.js (CoreCodery.com)

// อ่าน username ของตัวเอง
const { data: profile } = await supabase
  .from('profiles')
  .select('username')
  .eq('id', userId)
  .single();

// อัปเดต username (policy ป้องกัน user อื่น)
const { error } = await supabase
  .from('profiles')
  .update({ username: newUsername })
  .eq('id', userId);

ทดสอบ Policy ผ่าน Supabase Dashboard

วิธีที่ 1: SQL Editor — ทดสอบเป็น anon

SET LOCAL ROLE anon;
SELECT * FROM profiles;

วิธีที่ 2: SQL Editor — ทดสอบเป็น authenticated user

SET LOCAL "request.jwt.claims" TO '{"sub": "your-user-uuid", "role": "authenticated"}';
SELECT * FROM profiles WHERE id = 'your-user-uuid';

วิธีที่ 3: Policy Tester (Supabase Dashboard)

ไปที่ Table Editor → เลือก table → Policies แล้วคลิก "Test Policy" — ระบุ user ID และ operation ที่ต้องการทดสอบ


Gotchas ที่นักพัฒนาเจอบ่อย

1. Enable RLS แล้วข้อมูลหาย

-- Enable RLS แต่ไม่มี policy → ทุก query คืน 0 rows
ALTER TABLE my_table ENABLE ROW LEVEL SECURITY;
-- ลืมสร้าง policy!

-- แก้ไข:
CREATE POLICY "allow_select" ON my_table FOR SELECT USING (true);

2. INSERT ไม่ผ่งทั้งที่คิดว่า policy ถูก

-- ผิด: INSERT ต้องใช้ WITH CHECK ไม่ใช่ USING
CREATE POLICY "wrong" ON profiles FOR INSERT
  USING (auth.uid() = id);  -- ไม่มีผลกับ INSERT!

-- ถูก:
CREATE POLICY "correct" ON profiles FOR INSERT
  WITH CHECK (auth.uid() = id);

3. service_role ใน client code

// อันตราย: service_role bypass ทุก RLS policy
const adminClient = createClient(url, serviceRoleKey);  // อย่าใช้ใน client-side!

// ถูกต้อง: ใช้ anon key สำหรับ client-side
const client = createClient(url, anonKey);

4. UPDATE ต้องมีทั้ง USING และ WITH CHECK

CREATE POLICY "Users can update own profile"
  ON profiles FOR UPDATE
  USING (auth.uid() = id)        -- กรอง rows ที่อ่านได้
  WITH CHECK (auth.uid() = id);  -- ตรวจสอบ rows ที่เขียนได้

ถ้ามีแค่ `USING` — user อาจ update `id` ให้เป็น UUID คนอื่นได้


สรุป

RLS เป็นเครื่องมือที่ขาดไม่ได้สำหรับ Supabase app จริง:

  • Enable RLS บน table ที่มี sensitive data ทุก table
  • ใช้ `auth.uid()` สำหรับ user-specific data
  • ใช้ `USING` กับ SELECT/UPDATE/DELETE, ใช้ `WITH CHECK` กับ INSERT/UPDATE
  • ทดสอบด้วย SQL Editor ก่อน deploy จริง
  • อย่า ใช้ `service_role` key ใน client-side code

ตัวอย่าง `profiles` table ใน Pill Timer และ CoreCodery.com เป็น pattern ที่ balance ระหว่าง public readability กับ private write access — นำไปปรับใช้ได้เลย


*CoreCodery — Built in Thailand*

Enjoyed this? Get notified of new posts.

Weekly tutorials on Flutter, Cloudflare & Supabase — free.

No spam. Unsubscribe anytime.