PostgreSQL row-level security untuk SaaS: kebijakan yang efektif
Row-level security PostgreSQL untuk SaaS membantu menegakkan isolasi tenant di database. Pelajari kapan menggunakannya, cara menulis kebijakan, dan yang harus dihindari.

Masalah nyata yang coba diselesaikan RLS di aplikasi SaaS
Di aplikasi SaaS, bug keamanan paling berbahaya sering muncul setelah Anda melakukan scale. Anda mulai dengan aturan sederhana seperti “pengguna hanya bisa melihat data tenant mereka,” lalu Anda cepat menambahkan endpoint baru, menambahkan query reporting, atau memperkenalkan join yang diam-diam melewatkan pengecekan.
Otorisasi hanya di level aplikasi mudah pecah karena aturannya tersebar. Satu controller memeriksa tenant_id, controller lain memeriksa membership, job latar lupa, dan jalur “admin export” tetap "sementara" selama berbulan-bulan. Bahkan tim yang hati-hati bisa melewatkan satu titik.
PostgreSQL row-level security (RLS) memecahkan masalah spesifik: ia membuat database menegakkan baris mana yang terlihat untuk permintaan tertentu. Model mentalnya sederhana: setiap SELECT, UPDATE, dan DELETE otomatis difilter oleh kebijakan, seperti setiap permintaan difilter oleh middleware otentikasi.
Bagian "baris" itu penting. RLS tidak melindungi semuanya:
- Ia tidak menyembunyikan kolom tertentu sendirian (gunakan view atau hak akses kolom).
- Ia tidak membuat fungsi yang tidak aman menjadi aman (fungsi masih bisa membocorkan data jika dijalankan dengan hak istimewa lebih tinggi).
- Ia tidak memvalidasi aturan bisnis (misalnya, “hanya pemilik yang bisa mengubah pengaturan billing”).
Contoh konkret: Anda menambahkan endpoint yang menampilkan daftar projects dengan join ke invoices untuk dashboard. Dengan otorisasi yang hanya di aplikasi, mudah untuk memfilter projects berdasarkan tenant tetapi lupa memfilter invoices, atau melakukan join pada key yang melewati tenant. Dengan RLS, kedua tabel bisa menegakkan isolasi tenant, sehingga query gagal aman daripada membocorkan data.
Trade-off-nya nyata. Anda menulis lebih sedikit kode otorisasi berulang dan mengurangi jumlah tempat yang bisa bocor. Tapi Anda juga mengambil pekerjaan baru: Anda harus merancang kebijakan dengan hati-hati, mengetesnya lebih awal, dan menerima bahwa sebuah kebijakan bisa memblokir query yang Anda harapkan bekerja.
Kapan RLS menyederhanakan otorisasi (dan kapan menambah sakit kepala)
RLS terasa seperti kerja tambahan sampai aplikasi Anda melewati beberapa endpoint. Jika Anda punya batas tenant yang ketat dan banyak jalur query (layar daftar, pencarian, ekspor, alat admin), menaruh aturan di database berarti Anda tidak harus ingat menambahkan filter yang sama di mana-mana.
RLS cocok kuat ketika aturan itu membosankan dan universal: “pengguna hanya bisa melihat baris untuk tenant mereka” atau “pengguna hanya bisa melihat projects yang mereka menjadi anggotanya.” Dalam pengaturan seperti itu, kebijakan mengurangi kesalahan karena setiap SELECT, UPDATE, dan DELETE melewati pintu yang sama, bahkan ketika sebuah query ditambahkan kemudian.
Ini juga membantu pada aplikasi yang banyak baca (read-heavy) di mana logika penyaringan konsisten. Jika API Anda punya 15 cara berbeda memuat invoices (berdasarkan status, tanggal, customer, pencarian), RLS memungkinkan Anda berhenti mengimplementasikan penyaringan tenant pada setiap query dan fokus pada fitur.
RLS menambah sakit ketika aturannya bukan berbasis baris. Aturan per-field seperti "Anda bisa melihat gaji tapi bukan bonus" atau "mask kolom ini kecuali Anda HR" sering berubah menjadi SQL yang canggung dan pengecualian yang sulit dipelihara.
Ini juga kurang cocok untuk pelaporan berat yang memang membutuhkan akses luas. Tim sering membuat role bypass untuk "hanya pekerjaan ini saja," dan di situlah kesalahan menumpuk.
Sebelum berkomitmen, putuskan apakah Anda ingin database menjadi penjaga gerbang akhir. Jika ya, rencanakan disiplin: uji perilaku database (bukan hanya respons API), perlakukan migrasi sebagai perubahan keamanan, hindari bypass cepat, tentukan bagaimana job latar mengautentikasi, dan jaga kebijakan tetap kecil dan dapat diulang.
Jika Anda menggunakan tooling yang menghasilkan backend, itu bisa mempercepat delivery, tapi tidak menghilangkan kebutuhan untuk peran yang jelas, tes, dan model tenant sederhana. (Misalnya, Koder.ai menggunakan Go dan PostgreSQL untuk backend yang digenerasi, dan Anda tetap ingin merancang RLS dengan sengaja daripada “menaburnya nanti.”)
Dasar model data yang membuat kebijakan RLS mudah dikelola
RLS paling mudah ketika skema Anda sudah jelas menunjukkan siapa yang memiliki apa. Jika Anda mulai dengan model kabur dan mencoba “memperbaiki di kebijakan,” biasanya Anda mendapatkan query lambat dan bug yang membingungkan.
Taruh kunci tenant di semua tempat yang semestinya
Pilih satu kunci tenant (seperti org_id) dan gunakan secara konsisten. Sebagian besar tabel yang dimiliki tenant seharusnya memilikinya, bahkan jika mereka juga mereferensikan tabel lain yang memilikinya. Ini menghindari join di dalam kebijakan dan menjaga pemeriksaan USING tetap sederhana.
Aturan praktis: jika sebuah baris harus hilang ketika pelanggan membatalkan, besar kemungkinan baris itu perlu org_id.
Modelkan membership secara eksplisit
Kebijakan RLS biasanya menjawab satu pertanyaan: “Apakah pengguna ini anggota org ini, dan apa yang bisa mereka lakukan?” Itu sulit disimpulkan dari kolom ad-hoc.
Jaga tabel inti tetap kecil dan sederhana:
users(satu baris per orang)orgs(satu baris per tenant)org_memberships(user_id,org_id,role,status)- opsional:
project_membershipsuntuk akses per-project
Dengan itu, kebijakan Anda bisa memeriksa membership dengan satu lookup yang terindeks.
Pisahkan data referensi bersama dari data milik tenant
Tidak semuanya perlu org_id. Tabel referensi seperti countries, product categories, atau plan types sering dibagikan ke semua tenant. Jadikan mereka read-only untuk sebagian besar role, dan jangan kaitkan ke satu org.
Data milik tenant (projects, invoices, tickets) sebaiknya menghindari menarik detail tenant-spesifik melalui tabel bersama. Jaga tabel bersama minimal dan stabil.
FK, cascade, dan indexing
Foreign key tetap bekerja dengan RLS, tapi penghapusan bisa mengejutkan jika role yang melakukan delete tidak bisa “melihat” baris dependent. Rencanakan cascade dengan hati-hati dan uji alur delete nyata.
Index kolom yang difilter oleh kebijakan Anda, khususnya org_id dan kunci membership. Sebuah kebijakan yang terlihat seperti WHERE org_id = ... tidak boleh menjadi full-table scan ketika tabel mencapai jutaan baris.
Cara kerja kebijakan RLS, tanpa bagian menakutkan
RLS adalah saklar per-tabel. Setelah diaktifkan, PostgreSQL berhenti mempercayai kode aplikasi Anda untuk mengingat filter tenant. Setiap SELECT, UPDATE, dan DELETE difilter oleh kebijakan, dan setiap INSERT dan UPDATE divalidasi oleh kebijakan.
Perubahan mental terbesar: dengan RLS aktif, query yang dulu mengembalikan data bisa mulai mengembalikan nol baris tanpa error. Itu PostgreSQL melakukan kontrol akses.
Apa yang sebenarnya dilakukan kebijakan
Kebijakan adalah aturan kecil yang terikat ke tabel. Mereka menggunakan dua pemeriksaan:
USINGadalah filter baca. Jika sebuah baris tidak cocok denganUSING, baris itu tak terlihat untukSELECT, dan tidak bisa ditarget olehUPDATEatauDELETE.WITH CHECKadalah gerbang tulis. Ia menentukan baris baru atau yang diubah apa yang diizinkan untukINSERTatauUPDATE.
Pola SaaS umum: USING memastikan Anda hanya melihat baris dari tenant Anda, dan WITH CHECK memastikan Anda tidak bisa memasukkan baris ke tenant lain dengan menebak tenant ID.
PERMISSIVE vs RESTRICTIVE, dalam satu kalimat
Ketika Anda menambahkan lebih banyak kebijakan nanti, hal ini penting:
PERMISSIVE(default): sebuah baris diizinkan jika ada kebijakan yang mengizinkannya.RESTRICTIVE: sebuah baris diizinkan hanya jika semua kebijakan restrictive mengizinkannya (di atas perilaku permissive).
Jika Anda berencana melapisi aturan seperti kecocokan tenant plus pemeriksaan peran plus membership project, kebijakan restrictive bisa membuat maksud lebih jelas, tapi mereka juga membuatnya lebih mudah mengunci diri keluar jika Anda lupa satu kondisi.
Bagaimana Postgres tahu siapa penggunanya
RLS perlu nilai “siapa yang memanggil” yang dapat diandalkan. Opsi umum:
- Variabel session yang disetel per permintaan (misalnya,
app.user_iddanapp.tenant_id). - Klaim JWT yang dipetakan ke setting session oleh lapisan API Anda.
- Pergantian role (
SET ROLE ...per permintaan), yang bisa bekerja tetapi menambah overhead operasional.
Pilih satu pendekatan dan terapkan di mana-mana. Mencampur sumber identitas antar layanan adalah jalan cepat menuju bug yang membingungkan.
Menamai kebijakan supaya bisa debug nanti
Gunakan konvensi yang dapat diprediksi sehingga dump skema dan log tetap terbaca. Misalnya: {table}__{action}__{rule}, seperti projects__select__tenant_match.
Langkah demi langkah: tambahkan RLS ke satu tabel dan buktikan bekerja
Jika Anda baru di RLS, mulai dengan satu tabel dan bukti kecil. Tujuannya bukan cakupan sempurna. Tujuannya membuat database menolak akses lintas-tenant bahkan ketika ada bug di aplikasi.
Tabel kecil untuk latihan
Asumsikan tabel projects sederhana. Pertama, tambahkan tenant_id dengan cara yang tidak merusak penulisan.
ALTER TABLE projects ADD COLUMN tenant_id uuid;
-- Backfill existing rows (example: everyone belongs to a default tenant)
UPDATE projects SET tenant_id = '11111111-1111-1111-1111-111111111111'::uuid
WHERE tenant_id IS NULL;
ALTER TABLE projects ALTER COLUMN tenant_id SET NOT NULL;
Selanjutnya, pisahkan kepemilikan dari akses. Polanya umum: satu role memiliki tabel (app_owner), role lain digunakan oleh API (app_user). Role API seharusnya bukan pemilik tabel, atau ia bisa melewati kebijakan.
ALTER TABLE projects OWNER TO app_owner;
REVOKE ALL ON projects FROM PUBLIC;
GRANT SELECT, INSERT, UPDATE, DELETE ON projects TO app_user;
Sekarang tentukan bagaimana permintaan memberi tahu Postgres tenant yang dilayani. Salah satu cara sederhana adalah setting yang bersifat request-scoped. Aplikasi Anda menyetelnya segera setelah membuka transaksi.
-- inside the same transaction as the request
SELECT set_config('app.current_tenant', '22222222-2222-2222-2222-222222222222', true);
Aktifkan RLS dan mulai dengan akses baca.
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
CREATE POLICY projects_tenant_select
ON projects
FOR SELECT
TO app_user
USING (tenant_id = current_setting('app.current_tenant')::uuid);
Buktikan bekerja dengan mencoba dua tenant berbeda dan memeriksa bahwa jumlah baris berubah.
Tambahkan aturan tulis (WITH CHECK)
Kebijakan baca tidak melindungi tulis. Tambahkan WITH CHECK sehingga insert dan update tidak bisa menyelundupkan baris ke tenant yang salah.
CREATE POLICY projects_tenant_write
ON projects
FOR INSERT, UPDATE
TO app_user
WITH CHECK (tenant_id = current_setting('app.current_tenant')::uuid);
Cara cepat untuk memverifikasi perilaku (termasuk kegagalan) adalah menyimpan skrip SQL kecil yang bisa Anda jalankan ulang setelah setiap migrasi:
BEGIN; SET LOCAL ROLE app_user;SELECT set_config('app.current_tenant', '\u003ctenant A\u003e', true); SELECT count(*) FROM projects;INSERT INTO projects(id, tenant_id, name) VALUES (gen_random_uuid(), '\u003ctenant B\u003e', 'bad');(seharusnya gagal)UPDATE projects SET tenant_id = '\u003ctenant B\u003e' WHERE ...;(seharusnya gagal)ROLLBACK;
Jika Anda bisa menjalankan skrip itu dan mendapatkan hasil yang sama setiap kali, Anda punya baseline andal sebelum memperluas RLS ke tabel lain.
Pola kebijakan yang akan Anda gunakan ulang di sebagian besar aplikasi SaaS
Kebanyakan tim mengadopsi RLS setelah bosan mengulang pemeriksaan otorisasi yang sama di setiap query. Kabar baiknya: bentuk kebijakan yang Anda butuhkan biasanya konsisten.
Baris milik owner vs baris membership
Beberapa tabel secara alami dimiliki satu pengguna (notes, API tokens). Lainnya milik tenant di mana akses bergantung pada membership. Perlakukan ini sebagai pola berbeda.
Untuk data milik owner, kebijakan sering memeriksa created_by = app_user_id(). Untuk data tenant, kebijakan sering memeriksa apakah pengguna memiliki baris membership untuk org.
Cara praktis menjaga kebijakan terbaca adalah memusatkan identitas dalam helper SQL kecil dan menggunakannya kembali:
-- Example helpers
create function app_user_id() returns uuid
language sql stable as $$
select current_setting('app.user_id', true)::uuid
$$;
create function app_is_admin() returns boolean
language sql stable as $$
select current_setting('app.is_admin', true) = 'true'
$$;
Pisahkan aturan baca dari aturan tulis
Baca sering lebih luas daripada tulis. Misalnya, setiap anggota org bisa SELECT projects, tetapi hanya editor yang bisa UPDATE, dan hanya owner yang bisa DELETE.
Jaga eksplisit: satu kebijakan untuk SELECT (membership), satu kebijakan untuk INSERT/UPDATE dengan WITH CHECK (peran), dan satu untuk DELETE (seringkali lebih ketat dari update).
Override admin tanpa menonaktifkan RLS
Hindari “matikan RLS untuk admin.” Sebaiknya tambahkan jalur escape di dalam kebijakan, seperti app_is_admin(), sehingga Anda tidak sengaja memberi akses penuh ke role service bersama.
Soft delete dan flag status
Jika Anda menggunakan deleted_at atau status, masukkan itu ke dalam kebijakan SELECT (deleted_at is null). Kalau tidak, seseorang bisa “menghidupkan kembali” baris dengan membalik flag yang aplikasi anggap final.
UPSERT: buat WITH CHECK ramah
INSERT ... ON CONFLICT DO UPDATE harus memenuhi WITH CHECK untuk baris setelah penulisan. Jika kebijakan Anda membutuhkan created_by = app_user_id(), pastikan upsert mengatur created_by saat insert dan tidak menimpanya saat update.
Jika Anda menghasilkan kode backend, pola-pola ini layak dijadikan template internal sehingga tabel baru mulai dengan default aman daripada lembar kosong.
Foot-gun RLS umum yang membuat debugging menyakitkan
RLS hebat sampai satu detail kecil membuatnya terlihat seperti PostgreSQL “secara acak” menyembunyikan atau menampilkan data. Kesalahan di bawah ini membuang waktu paling banyak.
Foot-gun yang menyebabkan kebocoran tenant diam-diam
Perangkap pertama adalah lupa WITH CHECK pada insert dan update. USING mengontrol apa yang bisa Anda lihat, bukan apa yang boleh Anda buat. Tanpa WITH CHECK, bug aplikasi bisa menulis baris ke tenant yang salah, dan Anda mungkin tidak menyadarinya karena pengguna yang sama tidak bisa membacanya kembali.
Kebocoran umum lainnya adalah "leaky join." Anda memfilter projects dengan benar, lalu join ke invoices, notes, atau files yang tidak dilindungi dengan cara yang sama. Perbaikannya ketat tapi sederhana: setiap tabel yang bisa mengungkapkan data tenant membutuhkan kebijakan sendiri, dan view tidak boleh bergantung hanya pada satu tabel yang aman.
Polanya kegagalan umum muncul cepat:
- Kebijakan baca ada, tetapi kebijakan tulis tidak memiliki
WITH CHECK. - Kondisi kebijakan menggunakan join ke tabel lain yang tidak dilindungi.
- Akses ditegakkan di view, tetapi tabel bawahnya masih terbuka.
- Anda mengandalkan “aplikasi selalu mengatur tenant_id,” dan satu job latar lupa.
- Anda menguji dengan role superuser, jadi Anda tidak pernah melihat perilaku nyata.
Foot-gun yang membuat perilaku membingungkan
Kebijakan yang merujuk tabel yang sama (langsung atau melalui view) dapat menciptakan kejutan rekursi. Sebuah kebijakan mungkin memeriksa membership dengan menanyai view yang membaca tabel yang dilindungi lagi, menyebabkan error, query lambat, atau kebijakan yang tidak pernah cocok.
Setup role juga sumber kebingungan. Pemilik tabel dan role elevated bisa melewati RLS, sehingga tes Anda lulus sementara pengguna nyata gagal (atau sebaliknya). Selalu uji dengan role ber-privilege rendah yang digunakan aplikasi Anda.
Berhati-hatilah dengan fungsi SECURITY DEFINER. Mereka berjalan dengan hak pemilik fungsi, jadi helper seperti current_tenant_id() mungkin aman, tapi fungsi "convenience" yang membaca data bisa tanpa sengaja membaca lintas-tenant kecuali Anda merancangnya untuk menghormati RLS.
Juga setel search_path yang aman di dalam fungsi security definer. Jika tidak, fungsi bisa mengambil objek lain dengan nama yang sama, dan logika kebijakan Anda bisa diam-diam menunjuk ke hal yang salah bergantung pada state session.
Debugging RLS: cara praktis melihat apa yang terjadi
Bug RLS biasanya karena konteks yang hilang, bukan "SQL buruk." Sebuah kebijakan bisa benar di atas kertas tapi tetap gagal karena session role berbeda dari yang Anda kira, atau karena permintaan tidak pernah menyetel tenant dan user yang diharapkan kebijakan.
Cara andal mereproduksi laporan produksi adalah meniru setup session yang sama secara lokal dan menjalankan query yang sama. Itu biasanya berarti:
SET ROLE app_user;(atau role API nyata)SELECT set_config('app.tenant_id', 't_123', true);danSELECT set_config('app.user_id', 'u_456', true);- Jalankan SQL yang sama seperti yang dijalankan aplikasi Anda (termasuk parameter)
- Konfirmasi apa yang melihat Postgres:
SELECT current_user, current_setting('app.tenant_id', true), current_setting('app.user_id', true);
Ketika Anda tidak yakin kebijakan mana yang diterapkan, periksa katalog daripada menebak. pg_policies menunjukkan setiap kebijakan, perintah, dan ekspresi USING dan WITH CHECK. Padukan itu dengan pg_class untuk mengonfirmasi RLS diaktifkan di tabel dan tidak dilewati.
Masalah performa bisa terlihat seperti masalah otorisasi. Sebuah kebijakan yang melakukan join ke tabel membership atau memanggil fungsi mungkin benar tapi lambat ketika tabel tumbuh. Gunakan EXPLAIN (ANALYZE, BUFFERS) pada query yang direproduksi dan cari sequential scans, nested loops tak terduga, atau filter yang diterapkan terlambat. Index yang hilang pada (tenant_id, user_id) dan tabel membership sering menjadi penyebab.
Juga membantu untuk mencatat tiga nilai per permintaan di lapisan aplikasi: tenant ID, user ID, dan role database yang digunakan untuk permintaan. Ketika itu tidak cocok dengan yang Anda kira, RLS akan berperilaku "salah" karena inputnya salah.
Untuk tes, simpan beberapa tenant seed dan buat kegagalan menjadi eksplisit. Suite kecil biasanya mencakup: “Tenant A tidak bisa membaca Tenant B,” “user tanpa membership tidak bisa melihat project,” “owner bisa update, viewer tidak bisa,” “insert diblok kecuali tenant_id cocok dengan konteks,” dan “override admin hanya berlaku di tempat yang dimaksudkan.”
Daftar periksa cepat sebelum rilis RLS ke produksi
Perlakukan RLS seperti sabuk pengaman, bukan toggle fitur. Kekurangan kecil berubah menjadi “semua orang bisa melihat data semua orang” atau “semua mengembalikan nol baris.”
Bentuk data dan kebijakan
Pastikan desain tabel dan aturan kebijakan cocok dengan model tenant Anda.
- Setiap tabel milik-tenant harus memiliki kunci tenant jelas (biasanya
tenant_id). Jika tidak, tuliskan alasannya (mis. tabel referensi global). - Aktifkan RLS pada setiap tabel milik-tenant, bukan hanya yang “utama.” Jika beberapa jalur tidak boleh melewati RLS, pertimbangkan
FORCE ROW LEVEL SECURITYpada tabel tersebut. - Pisahkan aturan baca dan tulis. Baca menggunakan
USING. Tulis harus punyaWITH CHECKagar insert dan update tidak bisa memindahkan baris ke tenant lain. - Jaga predicate kebijakan agar ramah indeks. Jika kebijakan memfilter berdasarkan
tenant_idatau join melalui tabel membership, tambahkan index yang sesuai.
Skenario sanity sederhana: pengguna tenant A bisa membaca invoice mereka sendiri, bisa memasukkan invoice hanya untuk tenant A, dan tidak bisa mengubah invoice untuk mengganti tenant_id.
Role, performa, dan tes
RLS sekuat role yang dipakai aplikasi Anda.
- Konfirmasi aplikasi tidak pernah terhubung sebagai superuser, pemilik tabel, atau role dengan
bypassrls. - Jalankan beberapa query nyata dengan volume data mirip produksi dan periksa plan query.
- Tambahkan tes negatif otomatis yang membuktikan akses lintas-tenant gagal.
Contoh: aplikasi projects multi-tenant dengan akses berdasarkan membership
Bayangkan aplikasi B2B di mana perusahaan (org) punya projects, dan projects punya tasks. Pengguna bisa menjadi anggota beberapa org, dan seorang pengguna bisa menjadi anggota beberapa project tapi tidak semua. Ini cocok untuk RLS karena database dapat menegakkan isolasi tenant bahkan jika endpoint API lupa filter.
Model sederhana: orgs, users, org_memberships (org_id, user_id, role), projects (id, org_id), project_memberships (project_id, user_id), tasks (id, project_id, org_id, ...). org_id di tasks itu disengaja. Ia menjaga kebijakan sederhana dan mengurangi kejutan saat join.
Kebocoran klasik terjadi ketika tasks hanya punya project_id, dan kebijakan Anda memeriksa akses melalui join ke projects. Satu kesalahan (kebijakan permissive di projects, join yang menghilangkan kondisi, atau view yang mengubah konteks) bisa mengekspos tasks dari org lain.
Jalur migrasi yang lebih aman menghindari memutus traffic produksi:
- Kirim perubahan skema terlebih dahulu (tambahkan
org_idketasks, tambahkan tabel membership). - Backfill
tasks.org_iddariprojects.org_id, lalu tambahkanNOT NULL. - Tambahkan kebijakan tapi biarkan RLS dinonaktifkan sementara saat Anda menguji di staging.
- Aktifkan RLS, lalu paksa, dan baru hapus filter sisi aplikasi yang lama.
Akses support biasanya terbaik ditangani dengan role break-glass yang sempit, bukan dengan menonaktifkan RLS. Jaga terpisah dari akun support normal dan buat eksplisit saat dipakai.
Dokumentasikan aturan sehingga kebijakan tidak menyimpang: variabel session mana yang harus disetel (user_id, org_id), tabel mana yang harus membawa org_id, apa arti “member”, dan beberapa contoh SQL yang seharusnya mengembalikan 0 baris bila dijalankan sebagai org yang salah.
Langkah selanjutnya: jalankan RLS dengan aman dan pertahankan
RLS paling mudah dipakai ketika Anda memperlakukannya seperti perubahan produk. Rollout secara bertahap, buktikan perilaku dengan tes, dan simpan catatan jelas mengapa setiap kebijakan ada.
Rencana rollout yang biasanya berhasil:
- Mulai dengan satu tabel yang kepemilikannya jelas (misalnya,
projects) dan kunci itu. - Tambahkan tes yang mencakup baca dan tulis yang diizinkan dan diblokir untuk beberapa role (owner, member, outsider).
- Perluas dalam batch (satu area fitur tiap waktu) yang bisa Anda debug dalam satu session.
- Monitor error permission selama rollout dan deploy di jendela risiko rendah.
Setelah tabel pertama stabil, buat perubahan kebijakan menjadi sesuatu yang disengaja. Tambahkan langkah review kebijakan pada migrasi, dan sertakan catatan singkat tentang maksud (siapa yang harus mengakses apa dan mengapa) plus pembaruan tes yang sesuai. Ini mencegah "tambah OR lagi" yang perlahan-lahan berubah jadi lubang.
Jika Anda bergerak cepat, alat seperti Koder.ai (koder.ai) dapat membantu menghasilkan titik awal Go + PostgreSQL lewat chat, lalu Anda bisa melapisi kebijakan RLS dan tes dengan disiplin yang sama seperti backend yang dibuat tangan.
Akhirnya, jaga pengaman selama rollout. Ambil snapshot sebelum migrasi kebijakan, latih rollback sampai terasa biasa, dan pertahankan jalur break-glass kecil untuk support yang tidak mematikan RLS di seluruh sistem.
Pertanyaan umum
What security problem does RLS actually solve in a SaaS app?
RLS membuat PostgreSQL menegakkan baris mana yang terlihat atau dapat ditulis untuk sebuah permintaan, sehingga isolasi tenant tidak bergantung pada setiap endpoint yang selalu mengingat filter WHERE tenant_id = .... Keuntungan utamanya adalah mengurangi bug “satu cek yang terlewat” ketika aplikasi Anda tumbuh dan query bertambah.
When is RLS worth the extra complexity?
Biasanya sepadan ketika aturan akses konsisten dan berbasis baris, misalnya isolasi tenant atau akses berdasarkan membership, dan Anda punya banyak jalur query (pencarian, ekspor, layar admin, job latar). Tidak terlalu cocok jika aturan kebanyakan per-kolom, penuh pengecualian, atau ketika reporting lebar memang butuh pembacaan lintas-tenant.
What does RLS NOT protect me from?
Gunakan RLS untuk visibilitas baris dan pengendalian tulis dasar, lalu gunakan alat lain untuk hal lainnya. Privasi kolom biasanya perlu views dan hak akses kolom, dan aturan bisnis kompleks (mis. kepemilikan billing atau alur approval) tetap lebih cocok di logika aplikasi atau constraint basis data yang dirancang dengan hati-hati.
What’s the safest way to start using RLS if I’m new to it?
Buat role ber-privilege rendah untuk API (bukan pemilik tabel), aktifkan RLS, lalu tambahkan policy SELECT dan policy INSERT/UPDATE dengan WITH CHECK. Setel variabel session per permintaan (mis. app.current_tenant) dan verifikasi bahwa berpindah nilai itu mengubah baris yang bisa Anda lihat dan tulis.
How should my app tell Postgres which tenant and user is making the request?
Default yang umum adalah variabel session per permintaan, disetel di awal transaksi, seperti app.tenant_id dan app.user_id. Kunci utamanya adalah konsistensi: setiap jalur kode (web request, job, script) harus menyetel nilai yang sama yang diharapkan oleh policy, atau Anda akan mendapat perilaku "nol baris" yang membingungkan.
What’s the difference between USING and WITH CHECK in an RLS policy?
USING mengontrol baris yang sudah ada agar terlihat dan bisa ditarget untuk SELECT, UPDATE, dan DELETE. WITH CHECK mengontrol baris baru atau yang diubah yang diperbolehkan selama INSERT dan UPDATE, sehingga mencegah "menulis ke tenant lain" meskipun aplikasi mengirim tenant_id yang salah.
Why do people keep saying “don’t forget WITH CHECK”?
Karena jika Anda hanya menambahkan USING, endpoint bermasalah masih bisa INSERT atau UPDATE baris ke tenant yang salah, dan Anda mungkin tidak menyadarinya karena pengguna yang sama tidak bisa membaca baris itu kembali. Selalu pasangkan aturan baca tenant dengan WITH CHECK yang cocok untuk tulis agar data buruk tidak bisa dibuat sejak awal.
How should I structure my schema so RLS policies stay simple and fast?
Hindari join di dalam policy dengan menaruh kunci tenant (mis. org_id) langsung di tabel yang dimiliki tenant, bahkan jika tabel itu juga mereferensikan tabel lain yang memilikinya. Tambahkan tabel membership eksplisit (org_memberships, opsional project_memberships) sehingga policy bisa melakukan satu lookup terindeks daripada inferensi rumit.
How do I debug “RLS is hiding my data” without guessing?
Pertama, reproduksi konteks session yang sama seperti aplikasi Anda dengan menyetel role dan setting session yang sama, lalu jalankan query yang sama. Selanjutnya, pastikan RLS aktif dan periksa pg_policies untuk melihat ekspresi USING dan WITH CHECK yang diterapkan, karena kegagalan RLS sering disebabkan oleh konteks identitas yang hilang, bukan SQL yang salah.
If I generate my backend (for example with Koder.ai), do I still need to design RLS carefully?
Iya — perlakukan kode yang digenerasi sebagai titik awal, bukan sebagai sistem keamanan lengkap. Jika Anda menggunakan Koder.ai untuk menghasilkan backend Go + PostgreSQL, Anda tetap harus mendefinisikan model tenant, menyetel identitas session secara konsisten, dan menambahkan policy serta tes dengan sengaja supaya tabel baru tidak dikirim tanpa proteksi yang benar.