
Row level security là gì? Đó là cơ chế bảo mật theo từng dòng dữ liệu của PostgreSQL: thay vì cho phép một người dùng đọc cả bảng hay không được đọc gì cả, bạn viết luật ngay trong cơ sở dữ liệu để quyết định mỗi người được thấy và được sửa những dòng nào. Trên Supabase, RLS gần như bắt buộc, vì trình duyệt của người dùng gọi thẳng vào cơ sở dữ liệu — nếu bạn quên bật, bất kỳ ai mở mã nguồn trang web cũng có thể đọc được bảng của bạn.
Tôi viết bài này cho những người đang tự dựng web app bằng Supabase hoặc PostgreSQL, kể cả khi bạn nhờ AI viết phần lớn code. Đọc xong bạn sẽ bật được RLS, viết được policy cơ bản và quan trọng nhất là biết cách tự kiểm tra xem dữ liệu của mình có đang hở ra ngoài hay không.
Row level security là gì và vì sao web app cần nó?
Row Level Security (viết tắt RLS, tiếng Việt hay gọi là "bảo mật theo dòng") là tính năng của PostgreSQL cho phép gắn các policy — những điều kiện lọc — vào một bảng. Mỗi khi có truy vấn chạy vào bảng đó, PostgreSQL tự động ghép thêm điều kiện của policy, nên người dùng chỉ nhận về đúng phần dữ liệu họ được phép.
Hãy hình dung một công ty phân phối có bảng don_hang với các cột: id, ma_don, khach_hang, tong_tien, nhan_vien_id. Mỗi đơn hàng thuộc về một nhân viên kinh doanh phụ trách. Yêu cầu rất đời thường: nhân viên A chỉ được xem đơn của mình, nhân viên B chỉ xem đơn của B, còn trưởng phòng thì xem được tất cả.
Nếu phân quyền theo kiểu cũ — theo bảng — bạn chỉ có hai lựa chọn: cho nhân viên A đọc bảng don_hang (thấy luôn đơn của cả công ty) hoặc cấm hẳn (không làm việc được). Để giải quyết, phần lớn người tự làm app sẽ lọc ở phía ứng dụng, kiểu thêm điều kiện "chỉ lấy đơn của người đang đăng nhập" trong code. Cách đó chạy được, nhưng chỉ cần một màn hình nào đó quên thêm điều kiện, hoặc một API mới viết vội, là dữ liệu lộ ra ngoài mà không ai hay.
| Tiêu chí | Phân quyền theo bảng | Row Level Security (theo dòng) |
|---|---|---|
| Mức chi tiết | Được/không được đọc cả bảng | Từng dòng, theo điều kiện bạn đặt |
| Luật nằm ở đâu | Thường nằm rải rác trong code ứng dụng | Nằm trong cơ sở dữ liệu, dùng chung cho mọi đường vào |
| Quên một màn hình thì sao | Dữ liệu lộ hết | Vẫn bị chặn, vì luật áp dụng ở tầng dưới cùng |
| Chi phí vận hành | Dễ bắt đầu, khó kiểm soát khi app lớn | Tốn công thiết lập, nhưng an tâm về sau |
Nói ngắn gọn: RLS đưa luật phân quyền xuống chỗ sâu nhất — nơi mọi câu truy vấn đều phải đi qua. Đây là lý do nó đáng học ngay cả khi bạn không phải lập trình viên chuyên nghiệp. Nếu bạn còn mới với SQL, nên xem qua các câu lệnh SQL cơ bản cho người mới trước khi đọc tiếp phần dưới.
Row level security PostgreSQL hoạt động thế nào?
Ngắn gọn: bạn bật RLS cho bảng, rồi tạo policy mô tả điều kiện. Không có policy nào thì bảng bị khoá sạch. Cụ thể gồm ba ý:
- Bật RLS cho bảng:
ALTER TABLE don_hang ENABLE ROW LEVEL SECURITY;Từ lúc này, bảng chuyển sang chế độ "mặc định cấm". - Tạo policy:
CREATE POLICY ... FOR SELECT USING (điều kiện). PhầnUSINGlà bộ lọc: dòng nào làm điều kiện đúng thì người dùng mới thấy được. - Kiểm tra dữ liệu ghi vào: với
INSERTvàUPDATE, bạn dùngWITH CHECKđể quy định dòng mới ghi vào phải thoả điều kiện gì. Không cóWITH CHECKthì người dùng có thể ghi vào những dòng mà chính họ không được đọc.
Hai điểm rất hay làm người mới hoang mang:
- Bật RLS mà chưa có policy nào = chặn hết. App sẽ trả về danh sách rỗng chứ không báo lỗi. Nhiều người tưởng mất dữ liệu, thực ra dữ liệu vẫn nguyên, chỉ là chưa ai được phép nhìn.
- Chủ bảng và superuser không bị chặn. Role có quyền
BYPASSRLScũng vậy. Nên khi bạn chạy câu lệnh trong trình SQL Editor với quyền quản trị, bạn vẫn thấy đủ dữ liệu — đó không phải bằng chứng là policy của bạn sai hay đúng. PostgreSQL có tuỳ chọn ép áp dụng RLS cho cả chủ bảng; nếu cần, bạn nên tra tài liệu PostgreSQL chính thức cho đúng phiên bản đang dùng.

Supabase RLS: vì sao quan trọng hơn bạn nghĩ?
Với Supabase, RLS không phải tính năng "nên có" mà là lớp bảo vệ chính. Lý do nằm ở kiến trúc: Supabase cho phép trình duyệt gọi thẳng vào cơ sở dữ liệu qua API, dùng một chìa khoá gọi là anon key. Chìa khoá này nằm công khai trong mã nguồn trang web của bạn — ai bấm F12 cũng đọc được, và điều đó là bình thường, đúng thiết kế.
Điều không bình thường là: nếu bảng của bạn chưa bật row level security, thì người cầm anon key đó có thể gọi API để đọc, thậm chí sửa, dữ liệu trong bảng mà không cần đăng nhập. Tức là bảo mật của bạn lúc này chỉ dựa vào việc "chắc không ai để ý" — một kiểu may rủi.
Thêm một điểm sống còn: Supabase còn một chìa khoá nữa là service_role key, và chìa này bỏ qua toàn bộ RLS. Nó chỉ được phép nằm ở phía máy chủ (server, hàm backend, công cụ tự động hoá nội bộ). Tuyệt đối không đặt service_role key vào code frontend, không đưa vào biến môi trường của app chạy trên trình duyệt, không dán vào prompt khi nhờ AI sửa giao diện. Nếu lỡ lộ, hãy coi như toàn bộ cơ sở dữ liệu đã mở toang và phải tạo lại khoá ngay.
Quên bật RLS thì dữ liệu có bị lộ không?
Có. Với Supabase, một bảng nằm trong schema public mà tắt RLS thì về nguyên tắc là đọc được — và tuỳ quyền bạn cấp, có thể ghi được — qua API bằng anon key. Không cần hack, chỉ cần biết tên bảng và dán đúng đường dẫn API.
Ba cách tự kiểm tra, làm được trong mươi phút:
- Xem cảnh báo trong dashboard. Supabase có mục Security Advisor liệt kê các bảng đang để lộ, điển hình là cảnh báo bảng public chưa bật RLS. Đây là chỗ đầu tiên nên mở.
- Thử gọi API bằng anon key khi chưa đăng nhập. Mở một cửa sổ ẩn danh, dùng chính anon key trong code web của bạn để gọi lấy dữ liệu một bảng nhạy cảm (bảng khách hàng, bảng lương). Nếu nhận về dữ liệu thật, bạn đang bị lộ.
- Đừng quên view và function. RLS gắn vào bảng, không gắn vào view. Một view đọc từ bảng đã khoá vẫn có thể "lách" ra ngoài vì mặc định nó chạy bằng quyền của người tạo view. Tương tự với các hàm chạy theo quyền định nghĩa sẵn.
Theo quan sát của tôi khi xem lại các web app do anh em tự dựng bằng AI, lỗi phổ biến nhất không phải viết policy sai, mà là chưa bật RLS cho một vài bảng phụ — bảng log, bảng cấu hình, bảng nhập liệu tạm. Bảng chính thì nhớ khoá, bảng phụ thì quên, mà bảng phụ lại hay chứa đủ thông tin để ghép ngược ra dữ liệu chính.
Hướng dẫn bật row level security Supabase từng bước
Dưới đây là ví dụ hoàn chỉnh với bảng don_hang(id, ma_don, khach_hang, tong_tien, nhan_vien_id uuid), trong đó nhan_vien_id lưu đúng id tài khoản đăng nhập của nhân viên phụ trách.
- Bật RLS cho bảng. Vào SQL Editor và chạy:
alter table don_hang enable row level security;Ngay sau bước này, app của bạn sẽ trả về rỗng. Đó là dấu hiệu tốt, không phải lỗi. - Cho nhân viên xem đơn của chính mình.
create policy "nv xem don cua minh" on don_hang for select to authenticated using (nhan_vien_id = auth.uid());Trong đóauth.uid()trả về id của người đang đăng nhập, cònto authenticatedgiới hạn policy chỉ áp dụng cho người đã đăng nhập — khách vãng lai vẫn không thấy gì. - Cho nhân viên tạo đơn, nhưng chỉ đứng tên mình.
create policy "nv them don cua minh" on don_hang for insert to authenticated with check (nhan_vien_id = auth.uid());Nhờwith check, nhân viên không thể tạo đơn rồi gán cho người khác. - Test bằng hai tài khoản thật. Tạo hai tài khoản nhân viên, mỗi người thêm vài đơn, rồi đăng nhập chéo xem có nhìn thấy đơn của nhau không. Đây là bước không được bỏ: test bằng quyền quản trị trong SQL Editor sẽ cho kết quả sai lệch, vì chủ bảng vốn không bị RLS chặn.
Policy cho vai trò quản lý
Trưởng phòng cần thấy tất cả đơn. Cách thường dùng là tra vai trò từ một bảng nhân sự, ví dụ bảng nhan_su có cột id và vai_tro. Điều kiện trong policy sẽ là: exists (select 1 from nhan_su where id = auth.uid() and vai_tro = 'quan_ly'). Bạn có thể tạo thêm một policy SELECT riêng cho quản lý — nhiều policy cùng loại sẽ được cộng dồn, ai thoả policy nào thì thấy theo policy đó.
Một lưu ý về tốc độ: policy chạy kèm mọi truy vấn, nên cột dùng trong điều kiện phải có index, nếu không app sẽ chậm dần khi dữ liệu lớn lên. Với ví dụ trên, hãy đánh index cho cột nhan_vien_id: create index on don_hang (nhan_vien_id);
Bảng tóm tắt các loại policy RLS
Quy tắc dễ nhớ: USING lọc những dòng đang có (được thấy, được sửa, được xoá), còn WITH CHECK kiểm dòng mới sắp ghi vào.
| Thao tác | Dùng mệnh đề | Ý nghĩa | Ví dụ điều kiện |
|---|---|---|---|
| SELECT | USING | Những dòng nào được hiển thị | nhan_vien_id = auth.uid() |
| INSERT | WITH CHECK | Dòng mới phải thoả điều kiện mới cho ghi | nhan_vien_id = auth.uid() |
| UPDATE | USING + WITH CHECK | Được sửa dòng nào, và sau khi sửa phải còn hợp lệ | USING: đơn của mình — WITH CHECK: vẫn là đơn của mình |
| DELETE | USING | Được xoá những dòng nào | Chỉ quản lý: exists (select 1 from nhan_su where id = auth.uid() and vai_tro = 'quan_ly') |
Những lỗi hay gặp khi làm RLS
- Bật RLS xong app trả về rỗng. Gần như luôn là do chưa có policy, hoặc policy chỉ áp cho
authenticatedtrong khi màn hình đó chạy khi chưa đăng nhập. Kiểm tra danh sách policy của bảng trước khi nghi ngờ code. - Policy kiểu
using (true)cho nhanh. Điều kiện này luôn đúng, nghĩa là mở toàn bộ bảng — và nếu không giới hạn vai trò, mở luôn cho người chưa đăng nhập. Đây là cách "bật RLS" mà thực chất không bảo vệ gì cả. - Để service_role key ở frontend. Lỗi nặng nhất, vì khoá này bỏ qua mọi policy bạn vừa viết.
- Thiếu WITH CHECK ở UPDATE. Nhân viên sửa được đơn của mình, nhưng nếu không kiểm tra dòng sau khi sửa, họ có thể đổi
nhan_vien_idsang người khác hoặc ngược lại, kéo đơn của người khác về mình. - Quên view. View mặc định chạy bằng quyền của chủ view nên có thể lách qua RLS của bảng gốc. PostgreSQL có tuỳ chọn
security_invokerđể view chạy theo quyền người gọi — nếu app của bạn đọc dữ liệu qua view, hãy rà lại chỗ này. - Nhờ AI viết policy rồi dùng luôn. AI viết cú pháp rất nhanh và thường đúng cú pháp, nhưng nó không biết "ai được xem gì" trong công ty bạn. Tôi luôn coi policy do AI sinh ra là bản nháp, và chỉ tin sau khi đã đăng nhập bằng hai tài khoản thật để thử. Đây cũng là giới hạn chung của cách làm app bằng AI, tôi có nói kỹ hơn trong bài vibe coding là gì và những giới hạn cần biết.
Tóm lại và bước tiếp theo bạn nên làm
Row Level Security là cách đặt luật phân quyền ngay trong cơ sở dữ liệu, ở mức từng dòng. Với PostgreSQL, bạn bật bằng ALTER TABLE ... ENABLE ROW LEVEL SECURITY rồi tạo policy với USING và WITH CHECK. Với Supabase, đây là lớp bảo vệ bắt buộc, vì anon key vốn công khai, còn service_role key thì bỏ qua RLS nên chỉ được nằm phía máy chủ. Quên bật RLS đồng nghĩa với việc bảng của bạn có thể bị đọc từ bên ngoài.
Việc nên làm ngay hôm nay, theo thứ tự:
- Mở Security Advisor trong dashboard, liệt kê mọi bảng chưa bật RLS — kể cả bảng phụ.
- Bật RLS cho từng bảng, bắt đầu từ bảng chứa dữ liệu khách hàng và tiền.
- Viết policy SELECT và INSERT tối thiểu như ví dụ
don_hangở trên, nhớ kèmwith check. - Tạo hai tài khoản thử và đăng nhập chéo để xác nhận. Đây là bước duy nhất chứng minh policy chạy đúng.
- Rà lại xem service_role key có đang lọt vào code chạy trên trình duyệt không.
Nếu bạn đang ở giai đoạn sớm hơn — chưa chắc nên chia bảng thế nào, cột nào làm khoá — thì xử lý phần nền trước sẽ đỡ phải sửa policy về sau; tôi có hướng dẫn trong bài thiết kế cơ sở dữ liệu cho người mới: 6 bước làm đúng từ đầu. Còn nếu bạn vẫn đang mơ hồ chuyện app và dữ liệu của mình thực ra nằm ở đâu sau khi đưa lên mạng, bài deploy web app xong thì app và dữ liệu nằm ở đâu sẽ giải thích bằng ngôn ngữ đời thường.
Câu hỏi thường gặp
›Quên bật RLS thì dữ liệu có bị lộ không?
Có. Trên Supabase, bảng nằm trong schema public mà chưa bật row level security thì có thể bị đọc — và tuỳ quyền đã cấp, bị ghi — qua API bằng anon key, mà anon key vốn nằm công khai trong mã nguồn trang web. Hãy mở Security Advisor trong dashboard để xem danh sách bảng đang bị cảnh báo, và thử gọi API bằng anon key ở cửa sổ ẩn danh khi chưa đăng nhập để tự kiểm chứng.
›Bật RLS rồi sao app không hiện dữ liệu?
Vì bật RLS mà chưa có policy nào nghĩa là chặn hết: dữ liệu vẫn còn nguyên nhưng chưa ai được phép đọc. Bạn cần tạo ít nhất một policy FOR SELECT với điều kiện USING phù hợp. Một nguyên nhân khác là policy chỉ áp cho nhóm authenticated trong khi màn hình đó đang chạy ở trạng thái chưa đăng nhập.
›Mọi truy vấn đều đi qua server của mình thì có cần RLS không?
Không bắt buộc, nhưng vẫn rất nên có. Khi toàn bộ truy vấn đi qua backend do bạn kiểm soát, bạn có thể tự lọc dữ liệu trong code. Vấn đề là chỉ cần một API mới viết vội quên điều kiện lọc là dữ liệu lộ ra, trong khi RLS nằm ở tầng dưới cùng nên mọi đường vào đều bị áp dụng. Hãy coi RLS là lớp an toàn thứ hai.
›AI viết policy RLS có tin được không?
Tin được về cú pháp, không nên tin ngay về logic. AI không biết trong công ty bạn ai được xem đơn của ai, nên nó thường sinh ra điều kiện chung chung, đôi khi là USING (true) — tức mở toang bảng. Hãy coi policy do AI viết là bản nháp và chỉ chấp nhận sau khi đã đăng nhập bằng hai tài khoản thật với vai trò khác nhau để thử chéo.
Tác giả
Chuyên gia chuyển đổi số · Đại diện 5F Edu
Hơn 8 năm triển khai và tư vấn chuyển đổi số cho doanh nghiệp vừa và nhỏ: chuẩn hoá quy trình, xây phần mềm quản lý bằng AppSheet và web app, tự động hoá bằng N8N. Người làm kênh YouTube Nghiện AppSheet.



