Supabase RLS คืออะไร? ทำความเข้าใจ Row Level Security ฉบับนักพัฒนาไทย
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, UPDATEUSING 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.