Supabase RLS politikası nasıl yazılır: yapay zekanın ürettiği şemalar için 4 kalıp
RLS'i açmak sorunu yarı çözer. Geri kalanı tablonun gerçek ilişkisine uyan politikayı yazmak ve onun her sorguyu sessizce yavaşlatmasına izin vermemek.

RLS'i açmak sorunu yarı çözer. Asıl iş, tablonun gerçek ilişkisine uyan politikayı yazmak: satırı bir sahibi mi görür, bir takım mı, admin'in ayrıcalığı mı var. Yapay zeka araçlarının ürettiği şemalarda politika ya hiç yok ya da tek kalıba sıkışmış kopyalanmış oluyor, ikisi de production'da farklı şekilde patlıyor.
Özet
Supabase RLS politikası dört ilişkiye oturur: sahiplik, takım, admin ayrıcalığı, herkese açık okuma. Her birinin kendi sözdizimi ve kendi hata modu var. Politika doğru yazılsa bile auth.uid() her satırda yeniden çağrılıyor ve ilgili sütun indekslenmemişse sorgu 1000 kata kadar yavaşlıyor. Aşağıda dört kalıbın gerçek SQL'i, Supabase'in kendi ölçtüğü performans farkı ve pgTAP ile politika testi var.
RLS açık, politika var, ama yanlış: üç tipik hata
Tabloda "RLS aktif" yazısını görüp geçmek yetmiyor. Üç hata en sık tekrar ediyor, üçünde de teknik olarak bir politika var:
- WITH CHECK olmadan yazma politikası.
USINGokumayı kısıtlar.INSERTveUPDATEiçin ayrı birWITH CHECKyazılmazsa kullanıcı başkasının satırına veri yazabilir. - auth.uid()'in çıkış yapmış durumdaki halini unutmak. Kimliği doğrulanmamış bir istekte
auth.uid()nulldöner.null = user_idher zamanfalse'tur, yani satır hatasız şekilde görünmez olur. Sessizce boş dönen sonuç debug'ı zorlaştırıyor. - Doğru politika, yük altında sökülüyor. Production'da sorgu yavaşladığında ekip politikayı "geçici" kapatıyor ve geri açmayı unutuyor. Bu genelde sözdizimi hatası değil, aşağıdaki performans sorununun sonucu.
Yapay zeka araçlarının ürettiği şemalarda bu üçü birlikte görülüyor: politika var, ama tek seferde yazılıp kopyalanmış, gerçek ilişkiye hiç uydurulmamış. Doğru kalıbı seçmek için önce şunu sor.
Bu tabloya kim erişebilmeli?
Tek sahip, tek satır
Sahiplik kalıbı
Ortak bir grup sahip
Takım kalıbı
Bir rolün tam erişimi var
Admin kalıbı
Herkes okuyabilir
Herkese açık okuma
Kalıp 1: sahiplik, satırı sadece sahibi görür
En yaygın kalıp: bir satırın tek sahibi var, sadece o okur ve yazar. Profil, kişisel sipariş, özel not gibi tablolar buraya girer.
alter table public.profiles enable row level security;
create policy "sahip_kendi_satirini_okur"
on public.profiles for select
to authenticated
using ( (select auth.uid()) = user_id );
create policy "sahip_kendi_satirini_ekler"
on public.profiles for insert
to authenticated
with check ( (select auth.uid()) = user_id );
create policy "sahip_kendi_satirini_gunceller"
on public.profiles for update
to authenticated
using ( (select auth.uid()) = user_id )
with check ( (select auth.uid()) = user_id );
Dört ayrı politika yazıyoruz, tek bir "for all" politikası değil. select sadece using ister. insert sadece with check ister, çünkü henüz filtrelenecek mevcut bir satır yok. update ikisini de ister: using sorgunun hangi satıra dokunabileceğine karar verir, with check güncellemenin satırı yetkisiz bir sahibe taşımasını engeller.
auth.uid()'i (select ...) içine almak süsleme değil. Fonksiyonun her sorguda bir kez mi yoksa her satırda mı çalıştığı farkı yaratıyor, aşağıdaki performans bölümü nedenini gösteriyor.
Kalıp 2: takım ve çok kiracılı erişim
Satır bir kişiye değil bir takıma ait, üyelik bir join tablosunda tutuluyor. Neredeyse her yapay zeka üretimi çok kiracılı şema bu kalıba ihtiyaç duyuyor ve ilk denemede neredeyse hiç doğru çıkmıyor.
create policy "takim_uyesi_satiri_okur"
on public.documents for select
to authenticated
using (
team_id in (
select team_id from public.team_users
where user_id = (select auth.uid())
)
);
Karşılaştırmanın yönüne dikkat et: politika team_id'yi mevcut kullanıcıya göre çalışan bir alt sorguyla filtreliyor, tersi değil. auth.uid() in (select user_id from team_users where team_id = documents.team_id) şeklinde yazmak sözde aynı şeyi söylüyor, ama Postgres'i satır başına ilişkili bir alt sorgu çalıştırmaya zorluyor, kullanıcının takım kümesini bir kez çözmek yerine. Aynı mantık, çok farklı sorgu planı.
Kalıp 3: admin ayrıcalığı, service role olmadan
Bir admin paneli için service role key'e uzanmak, satır bazlı kontrolü tamamen kaybetmenin en hızlı yolu: bypassrls yetkisini taşıyor ve her tablodaki her politikayı atlıyor, geriye üzerinde düşünülecek hiçbir satır bazlı görünürlük kalmıyor. Politika içine yazılan bir is_admin() kontrolü admin erişimini role göre filtreli, denetlenebilir ve geri alınabilir tutuyor.
create or replace function public.is_admin()
returns boolean
language sql security definer
set search_path = public
as $$
select exists (
select 1 from public.admins where user_id = auth.uid()
);
$$;
create policy "sahip_veya_admin_satiri_okur"
on public.orders for select
to authenticated
using (
(select public.is_admin()) or (select auth.uid()) = user_id
);
Fonksiyon security definer olarak çalışıyor, bu yüzden admins tablosunu çağıranın adına okuyabiliyor, o tabloyu her kullanıcıya açan ayrı bir RLS politikasına gerek kalmadan. Fonksiyon çağrısını da (select ...) içine al, sahiplik kalıbındakiyle aynı önbellekleme sebebiyle.
Kalıp 4: herkese açık okuma, yetkiliye yazma
Bir blog, bir katalog, herkese açık bir liste: herkes okur, sadece giriş yapmış sahip ya da rol yazar. Tek politikayla ikisini birden çözmeye çalışmak yerine iki farklı to hedefiyle iki politikaya böl.
create policy "herkes_yayinlanani_okur"
on public.articles for select
to anon, authenticated
using ( status = 'published' );
create policy "yazar_kendi_yazisini_ekler"
on public.articles for insert
to authenticated
with check ( (select auth.uid()) = author_id );
Storage bucket'ları aynı dört kalıbı takip ediyor, ama bucket politikası bir tablo yerine storage.objects'i filtreliyor ve sahiplik sinyalini bir sütun yerine klasör yapısı taşıyor. Bu kendi başına ayrı bir konu; Lovable ve Bolt uygulamalarının neyi açıkta bıraktığı yazımız bucket'a özel politikayı baştan sona anlatıyor.
| Kalıp | Ne zaman kullanılır | Sık görülen AI hatası |
|---|---|---|
| Sahiplik | Tek kullanıcı, tek satır | Update'te WITH CHECK yok |
| Takım | Join tablosuyla paylaşılan satır | Satır başına tarama zorlayan alt sorgu yönü |
| Admin | Bir rolün ayrıcalığa ihtiyacı var | Kapsamlı kontrol yerine service role key |
| Herkese açık okuma | Herkes okur, sahip yazar | İki yönü tek politikayla çözmeye çalışmak |
Politikan sorguyu neden 1000 kat yavaşlatabilir
Supabase'in kendi troubleshooting dokümantasyonu, 100 bin satırlık bir tabloda ölçülmüş sayıları yayınlıyor. Fark neredeyse tamamen iki değişiklikten geliyor.
Kaynak: Supabase'in kendi RLS performans ve iyi pratikler dokümantasyonu.
Index kuralı net: bir politikanın using ya da with check'i içinde referans verilen her sütun, diğer sık filtreler gibi bir btree index istiyor.
create index on public.documents using btree (team_id);
(select ...) sarmalaması işe yarıyor çünkü Postgres bunu bir initPlan'a çevirebiliyor, sonucu satır başına fonksiyon çağırmak yerine sorgu için önbelleğe alıyor. Bu sadece sarmalanan değer satırdan satıra değişmiyorsa geçerli, ki auth.uid(), auth.jwt() ve is_admin() gibi bir security definer fonksiyonu için doğru. Her politikaya to authenticated eklemek de yardımcı oluyor: Postgres anon rolü için politikayı değerlendirip false almak yerine tamamen atlayabiliyor.
Politikayı gerçekten test etmek: pgTAP
"Doğru görünen" politika ile "gerçekten uygulanan" politika farklı iddialar. Supabase tam bunun için pgTAP desteği veriyor, CLI üzerinden çalıştırılıyor:
supabase test db
Bir test önce politikanın göreceği rolü ve kimliği ayarlıyor, sonra sonucu doğruluyor:
set local role authenticated;
set local request.jwt.claim.sub = '11111111-1111-1111-1111-111111111111';
select results_eq(
$$insert into profiles (id, user_id, avatar_url)
values (gen_random_uuid(), '11111111-1111-1111-1111-111111111111', 'owner.png')
returning avatar_url$$,
array['owner.png'],
'sahip kendi profilini olusturabilir'
);
Çoğu politikayı üç doğrulama şekli kapsıyor: reddedilmesi gereken bir yazma için 42501 izin hatasıyla dışarı atan throws_ok, hata fırlatmadan sessizce hiçbir şey döndürmesi gereken bir okuma için is_empty, ve bir yazmanın gerçekten işlendiğini kanıtlamak için returning ile results_eq. Reddetme durumunu, başarı durumu kadar özenle test et. Doğru kullanıcıyı içeri alan ama yanlış kullanıcıyı hiç reddetmeyen bir politikanın genelde hiç with check'i yoktur.
Bu kalıplar ne zaman yetmiyor
Yukarıdaki dört kalıp erişim kontrolünün satır seviyesindeki şeklini kapsıyor. Kapsamadığı şeyler: birden fazla tabloyu ağır join'lerle kapsayan bir iş kuralı (bir security definer fonksiyonu yardımcı olur, ama yaptığı her join yine kendi başına incelenmeli), denetim ve versiyonlama gereksinimleri, ya da ilişkileri ürünün gerçekten ihtiyaç duyduğuyla henüz uyuşmayan bir şema. Şemanın kendisi bir politika bir kuralı ifade edebilmeden önce şekil değiştirmesi gerekiyorsa, bu bir politika düzeltmesi değil, bir tasarım turudur.
Kod yapay zeka aracından çıkmış ve kimse tam olarak ne ürettiğini okumamışsa, yapay zeka projesi tamamlama sürecimiz tam bu okumayla başlıyor: hangi tablo hangi ilişkiyi taşıyor, hangi politika buna uyuyor, hangisi uymuyor.
Yayına almadan önce RLS kontrol listesi
Bu liste RLS'e özel. Daha geniş yüzey için (anahtarlar, dosya yükleme, webhook, loglama) yapay zeka ile yazılan uygulamalar için yayın öncesi güvenlik kontrol listesi yazımız geri kalanını kapsıyor.
publicşemasındaki her tabloda RLS açık, kolaylık için hiçbir istisna yok.- Her yazma politikasının sadece
using'i değil,with check'i de var. - Her politika rolü açık bırakmak yerine
to authenticated(ya daanon) belirtiyor. - Bir politikanın içinde referans verilen her sütunda btree index var.
- Politika içindeki her
auth.uid()veauth.jwt()çağrısı(select ...)içine alınmış. - Her tablo için en az bir pgTAP testi, sadece başarı değil, reddetme durumunu da doğruluyor.
Sık sorulan sorular
Service role key'i is_admin() politikası yerine ne zaman gerçekten kullanmalıyım?
Sadece satır filtrelemeyi tamamen atlaması gereken, güvenilir sunucu tarafı kodlardan: bir migration script'i ya da zamanlanmış bir job gibi. İstemciden erişilebilecek hiçbir şeyden değil, ve gerçek bir kullanıcının açtığı bir admin paneli için asla kısayol olarak değil.
auth.uid()'i her yerde select içine almam gerekiyor mu?
Bir politikanın using ya da with check'i içinde göründüğü her yerde evet, değer kontrol edilen satıra bağlı olmadığı sürece. Bu auth.uid(), auth.jwt() ve çoğu security definer yardımcı fonksiyonu kapsıyor.
pgTAP yerel bir Supabase kurulumu gerektiriyor mu?
Supabase CLI üzerinden yerel geliştirme veritabanına karşı çalışıyor, yani ayrı bir test servisi değil, CLI ve çalışan bir yerel stack gerekiyor.
RLS her zaman sorgu performansından götürür mü?
İndekslenmiş ve doğru sarmalanmış bir politika normal bir WHERE koşuluna yakın bir yük ekliyor. Yukarıdaki 1000 kat sayılar RLS'in kendisinden değil, eksik index'ten ya da satır başına yeniden çağrılan bir fonksiyondan geliyor.
Tablomda şu an sadece bir kalıp var. Yine de dördüne mi ihtiyacım var?
Hayır, tablonun gerçek ilişkisine uyanı seç. Dördü, geçen sefer kopyaladığın politikaya sığınmayı bırakman için var.