Supabase RLS คืออะไร พร้อมตัวอย่าง Policy | KittiStack
BlogBackend & Database Supabase RLS คืออะไร พร้อมตัวอย่าง Policy เข้าใจการทำงานของ Supabase RLS ผ่านตัวอย่างตารางงานส่วนตัว ตั้งแต่แยกหน้าที่ Auth กับ Policy ไปจนถึงเขียน SQL และทดสอบสิทธิ์ด้วยบัญชีผู้ใช้สองบัญชี
Kittipong Chankhotr
ทีมงาน KittiStack
October 28, 2026 · 7 min read
Supabase RLS คืออะไร พร้อมตัวอย่าง Policy Supabase RLS คืออะไร? RLS ย่อมาจาก Row Level Security เป็นกลไกของ PostgreSQL ที่กำหนดว่าผู้ใช้แต่ละคนอ่าน เพิ่ม แก้ไข หรือลบข้อมูลแถวไหนได้ โดยเขียนเงื่อนไขที่เรียกว่า Policy ไว้ในฐานข้อมูล เมื่อนำมาใช้กับ Supabase Auth เราจึงกำหนดให้สมาชิกเข้าถึงเฉพาะข้อมูลของตัวเองได้ แม้เรียก Data API จากหน้าเว็บโดยตรง
ตัวอย่างใกล้ตัวคือแอปบันทึกงาน สมาชิกทุกคนใช้งานตารางเดียวกัน แต่แต่ละคนควรเห็นเฉพาะงานของตัวเอง บทความนี้จะพาแยกหน้าที่ของ Auth, GRANT และ RLS ก่อนลงมือเขียน Policy ครบทั้ง SELECT, INSERT, UPDATE และ DELETE
Supabase RLS คืออะไร และช่วยแก้ปัญหาแบบไหน สมมติว่าเรามีตาราง tasks สำหรับเก็บงานของสมาชิกทุกคน แต่ละแถวมี user_id ระบุเจ้าของงาน หากแอปกรองข้อมูลด้วยรหัสผู้ใช้เฉพาะในหน้าเว็บ ผู้ใช้ที่แก้คำขอ API เป็นอาจเปลี่ยนเงื่อนไขเพื่อขอข้อมูลของคนอื่นได้
RLS ทำให้ฐานข้อมูลตรวจสิทธิ์อีกชั้นหนึ่ง เช่น อนุญาตให้อ่านเฉพาะแถวที่ user_id ตรงกับผู้ใช้ที่ยืนยันตัวตนแล้ว การลบตัวกรองออกจากคำขอจึงไม่ได้ทำให้ผู้ใช้ทั่วไปอ่านข้อมูลทุกคนได้ หาก Policy ถูกกำหนดไว้อย่างเหมาะสม
ผู้ใช้เรียกตารางเดียวกัน แต่ RLS จำกัดแถวข้อมูลตามสิทธิ์ของแต่ละคน
สำหรับคนที่ยังไม่คุ้นกับบริการนี้ อ่านพื้นฐานได้ที่ Supabase คืออะไรและเหมาะกับงานแบบไหน
#Supabase #PostgreSQL #Authentication #Row Level Security #Database Security
Auth, GRANT และ RLS ต่างกันอย่างไร การล็อกอินสำเร็จไม่ได้แปลว่าผู้ใช้ควรอ่านข้อมูลทุกแถวได้ การควบคุมสิทธิ์ใน Supabase ควรแยกพิจารณาเป็นสามส่วน
ถ้าไม่มีสิทธิ์ระดับตาราง คำขออาจถูกปฏิเสธก่อนตรวจ Policy ส่วนกรณีมีสิทธิ์ SELECT แต่ไม่มีแถวที่ผ่าน RLS ผลลัพธ์อาจเป็นรายการว่าง ทั้งสองอาการจึงต้องแก้คนละจุด
เมื่อเปิด RLS แล้วแต่ยังไม่มี Policy PostgreSQL จะใช้หลักปฏิเสธการเข้าถึงแถวเป็นค่าเริ่มต้นสำหรับบทบาทที่อยู่ภายใต้ RLS อย่างไรก็ตาม เจ้าของตารางมักข้าม RLS ได้ และบทบาทที่มี BYPASSRLS ก็เป็นข้อยกเว้น จึงไม่ควรสรุปว่าเปิด RLS แล้วทุกการเชื่อมต่อจะถูกจำกัดเหมือนกัน
USING กับ WITH CHECK ใช้ต่างกันอย่างไร Policy คือเงื่อนไข SQL ที่ใช้ตัดสินว่าการเข้าถึงข้อมูลได้รับอนุญาตหรือไม่ คำสำคัญที่เจอบ่อยคือ USING และ WITH CHECK ซึ่งตรวจข้อมูลคนละช่วง
ส่วน auth.uid() เป็นฟังก์ชันที่คืนรหัสผู้ใช้จากบริบทการยืนยันตัวตนของคำขอ ไม่ใช่ค่าที่เชื่อจากฟิลด์ user_id ในข้อมูลที่ client ส่งมา หากคำขอไม่มีผู้ใช้ที่ยืนยันตัวตนแล้ว ฟังก์ชันจะคืน NULL ทำให้เงื่อนไขเปรียบเทียบเจ้าของข้อมูลไม่ผ่าน
ตัวอย่าง Supabase RLS สำหรับตารางงานส่วนตัว ตัวอย่างนี้ใช้ Supabase Auth และสร้างตาราง public.tasks ใหม่ โดยกำหนดให้สมาชิกอ่าน เพิ่ม แก้ไข และลบได้เฉพาะงานของตัวเอง ส่วนผู้ที่ยังไม่ล็อกอินไม่มีสิทธิ์เข้าถึงตารางนี้
รัน SQL ผ่าน SQL Editor ในโปรเจกต์ทดลอง หรือบันทึกเป็น migration หากมีตารางชื่อเดียวกันอยู่แล้ว ให้ปรับชื่อและโครงสร้างให้ตรงกับระบบก่อน ตัวอย่างนี้มีไว้สร้างตารางใหม่ ไม่ใช่สคริปต์สำหรับรันซ้ำบนตารางเดิม
1. สร้างตาราง เปิด RLS และกำหนดสิทธิ์ begin;
create table public.tasks (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references auth.users(id),
title text not null ,
is_done boolean not null default false ,
created_at timestamptz not null default now()
);
alter table public.tasks enable row level security;
revoke all on table public.tasks from anon, authenticated;
grant select, insert, update, delete
on table public.tasks
to authenticated;
create index tasks_user_id_idx
on public.tasks (user_id);
create policy "tasks_select_own"
on public.tasks
for select
to authenticated
using ((select auth.uid()) = user_id);
create policy "tasks_insert_own"
on public.tasks
for insert
to authenticated
with check ((select auth.uid()) = user_id);
create policy "tasks_update_own"
on public.tasks
for update
to authenticated
using ((select auth.uid()) = user_id)
with check ((select auth.uid()) = user_id);
create policy "tasks_delete_own"
on public.tasks
for delete
to authenticated
using ((select auth.uid()) = user_id);
commit;การใช้ begin และ commit ทำให้การสร้างตาราง กำหนดสิทธิ์ และเพิ่ม Policy อยู่ใน transaction เดียวกัน ส่วน revoke และ grant ช่วยกำหนดสิทธิ์ให้ชัดเจน โดยไม่ต้องพึ่งค่าเริ่มต้นของแต่ละโปรเจกต์
ดัชนีบน user_id รองรับการค้นหาตามเจ้าของงาน ขณะที่รูปแบบ (select auth.uid()) ช่วยให้ PostgreSQL มีโอกาสประเมินค่าผู้ใช้ครั้งเดียวต่อคำสั่งเมื่อเหมาะสม แทนการเรียกซ้ำทุกแถว ประสิทธิภาพจริงยังต้องวัดจาก query และข้อมูลของแอป
2. ทำความเข้าใจ Policy ตอนแก้ไขข้อมูล ใน UPDATE Policy เงื่อนไข USING ตรวจว่าผู้ใช้เป็นเจ้าของแถวเดิมหรือไม่ ส่วน WITH CHECK ตรวจว่าแถวหลังแก้ไขยังมีเจ้าของตรงกับผู้ใช้เดิม จึงป้องกันการแก้งานของตัวเองแล้วเปลี่ยน user_id ไปเป็นของคนอื่น
ถ้าไม่ได้ระบุ WITH CHECK ใน UPDATE Policy PostgreSQL สามารถนำเงื่อนไข USING มาใช้ตรวจแถวใหม่ได้ แต่การเขียนแยกให้เห็นทั้งสองส่วนช่วยให้ตรวจทานเจตนาได้ง่ายกว่า นอกจากนี้ การอัปเดตผ่าน Supabase ควรมี SELECT Policy ที่อนุญาตให้อ่านแถวเป้าหมายด้วย
3. เรียกข้อมูลจากแอปหลังล็อกอิน สมมติว่าตัวแปร supabase เป็น client ที่ใช้ publishable key และมี session ของผู้ใช้ที่ล็อกอินแล้ว เราสามารถเรียกข้อมูลได้ดังนี้
const { data, error } = await supabase
.from('tasks')
.select('id, title, is_done')
.order('created_at', { ascending: false } );
if (error) throw error;
console.log(data);แม้ตัวอย่างไม่ได้ใส่ตัวกรอง user_id ใน client แต่ SELECT Policy จะจำกัดผลลัพธ์ให้เหลือเฉพาะแถวที่ผู้ใช้มีสิทธิ์อ่าน การเพิ่มตัวกรองในแอปยังทำได้เพื่อเลือกชุดข้อมูลที่ต้องการ แต่ขอบเขตสิทธิ์ต้องบังคับด้วยฐานข้อมูล
วิธีทดสอบว่าอ่านข้อมูลข้ามบัญชีไม่ได้ เตรียมบัญชี A และ B ผ่าน Supabase Auth แล้วให้แต่ละบัญชีสร้างงานของตัวเองอย่างน้อยหนึ่งรายการ ทดสอบผ่าน client ที่ใช้ session ของบัญชีเหล่านั้นจริง และแยก browser profile หรือ session ให้ชัดเจน
จากนั้นสลับทดสอบด้วยบัญชี B อีกครั้ง อย่าตรวจเฉพาะว่าคำขอมี error หรือไม่ เพราะ UPDATE หรือ DELETE ที่มองไม่เห็นแถวเป้าหมายอาจจบโดยไม่มี error แต่เปลี่ยนข้อมูลศูนย์แถว ควรอ่านข้อมูลกลับด้วยบัญชีเจ้าของเพื่อยืนยันด้วย
การรัน SELECT ใน SQL Editor ด้วยบทบาทผู้ดูแลเพียงอย่างเดียวไม่ใช่หลักฐานว่า Policy ของผู้ใช้ทำงานถูกต้อง สำหรับโปรเจกต์ที่ดูแลต่อเนื่อง ควรเพิ่มการทดสอบฐานข้อมูลที่ระบุบทบาทและตัวตนให้ครอบคลุมทั้งกรณีอนุญาตและปฏิเสธ
ข้อผิดพลาดที่พบบ่อยเมื่อใช้ RLS
ใช้กุญแจสิทธิ์สูงในหน้าเว็บหรือทดสอบผิดบทบาท Publishable key ออกแบบให้ใช้ใน client ได้ โดยต้องกำหนด grants และ Policy ให้เหมาะสม ส่วน secret key และกุญแจ service_role แบบเดิมเป็นกุญแจสิทธิ์สูงที่ข้าม RLS ได้ จึงต้องเก็บในสภาพแวดล้อมฝั่งเซิร์ฟเวอร์ที่ควบคุมได้ และไม่ใช้เป็นตัวแทนในการทดสอบสิทธิ์ผู้ใช้ทั่วไป
เปิดให้อ่านทุกแถวโดยไม่ได้ตั้งใจ เงื่อนไข USING (true) หมายถึงทุกแถวผ่านเงื่อนไขนั้นสำหรับบทบาทและคำสั่งที่ Policy ครอบคลุม จึงเหมาะกับข้อมูลที่ตั้งใจเปิด เช่น รายการสินค้าสาธารณะ แต่ต้องพิจารณาให้ดีหากเป็นข้อความส่วนตัวหรือข้อมูลสมาชิก
ตรวจ Policy เดิมในตารางด้วย เพราะ Policy แบบ permissive ซึ่งเป็นค่าเริ่มต้นจะรวมเงื่อนไขแบบ OR การเพิ่ม Policy ที่รัดกุมอีกอันจึงไม่ได้ทำให้ Policy เก่าที่เปิดกว้างหยุดทำงาน
เข้าใจว่าคุมแถวแล้วจะคุมทุกคอลัมน์ด้วย การอนุญาตให้แก้แถวของตัวเองไม่ได้แปลว่าผู้ใช้ควรแก้ได้ทุกคอลัมน์ หากตารางโปรไฟล์มี role หรือยอดเครดิต การตรวจเจ้าของแถวเพียงอย่างเดียวอาจยังไม่เพียงพอ ต้องใช้สิทธิ์ระดับคอลัมน์ เงื่อนไขเพิ่มเติม หรือแยกข้อมูลที่ต้องใช้สิทธิ์สูงออกไป
ตัวอย่างตาราง tasks ในบทความตั้งใจให้เจ้าของจัดการงานของตัวเองได้ แต่หากต้องห้ามแก้ id หรือ created_at ควรจำกัดสิทธิ์ UPDATE ให้เหลือเฉพาะคอลัมน์ที่แก้ได้จริง อ่านแนวคิดเพิ่มเติมที่ การออกแบบสิทธิ์ผู้ใช้ในแอปพลิเคชัน
คำถามที่พบบ่อย เปิด RLS แล้วดึงข้อมูลไม่ได้ ต้องปิดหรือไม่ให้ตรวจ session, GRANT และ SELECT Policy ก่อน รวมถึงค่า user_id ในข้อมูล หากได้รายการว่างอาจไม่มีแถวที่ผ่านเงื่อนไข แต่ถ้าได้ permission error อาจขาดสิทธิ์ระดับตาราง ไม่จำเป็นต้องปิด RLS เพื่อแก้ปัญหา
ใช้ RLS แล้วต้องมี Backend อีกไหมงาน CRUD บางส่วนเรียก Data API จาก client ได้เมื่อกำหนดสิทธิ์ครบ แต่งานที่ใช้ความลับ ติดต่อบริการภายนอกด้วยกุญแจส่วนตัว หรือทำงานด้วยสิทธิ์ผู้ดูแล ยังต้องทำบนเซิร์ฟเวอร์หรือฟังก์ชันฝั่งเซิร์ฟเวอร์
RLS ทำให้แอปช้าลงไหมRLS เพิ่มการประเมินเงื่อนไข แต่ผลกระทบขึ้นกับ Policy ปริมาณข้อมูล และดัชนี ควรวัด query จริง ตรวจ execution plan และพิจารณาดัชนีบนคอลัมน์ที่ใช้ตรวจสิทธิ์ แทนการคาดเดาว่าทุก Policy จะมีต้นทุนเท่ากัน
RLS ใช้กับระบบหลายบริษัทได้ไหมใช้ได้ โดยกำหนด Policy ให้ตรวจว่าผู้ใช้เป็นสมาชิกของบริษัทที่เป็นเจ้าของข้อมูลจริง อย่าอนุญาตจาก tenant_id หรือ company_id ที่ client ส่งมาเพียงอย่างเดียว และต้องทดสอบกรณีผู้ใช้พยายามเข้าถึงข้อมูลนอกบริษัทด้วย
เริ่มใช้ RLS จากตารางที่เก็บข้อมูลส่วนตัว เลือกตารางที่ระบุเจ้าของข้อมูลได้ชัดเจนก่อน ตั้ง GRANT เปิด RLS และเขียน Policy แยกตามคำสั่งที่แอปใช้ จากนั้นทดสอบด้วยบัญชีสองบัญชีให้มั่นใจว่าทำงานกับข้อมูลตัวเองได้ และเข้าถึงข้อมูลของอีกบัญชีไม่ได้ ก่อนนำรูปแบบเดียวกันไปปรับใช้กับตารางอื่น