Bu referans, beş üretim sistemi üzerinden Supabase çalıştırarak oluşturuldu — 600K LOC'luk bir AI işe alım platformu, çok kiracılı bir fuar SaaS'ı, gerçek zamanlı bir manevi pazar yeri, bir salon yönetim sistemi ve otonom bir otomat makine backend'i.
Her bölüm tek bir soruyu yanıtlar: Üretimde gerçekten ne önemli?
@supabase/ssr ile yeni asimetrik JWT anahtarı modelini (Mayıs 2025'ten itibaren varsayılan) ve yeni API anahtarı formatını (sb_publishable_xxx / sb_secret_xxx) kapsar. Eski anon ve service_role anahtarları geçiş süreci boyunca çalışmaya devam eder.
# .env.localNEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co# Yeni anahtar formatı (tercih edilen — Mayıs 2025+)NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_xxx# Eski anahtarlar (geçiş süreci boyunca çalışır)NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbG...
service_role / sb_secret_xxx anahtarlarını asla NEXT_PUBLIC_ ile başlatma — tarayıcıya gönderilir.
Alacağın en önemli API kararı bu. Üç metodun temelden farklı davranışları var:
Metod
Ağ çağrısı
Güven seviyesi
Kullanım durumu
getSession()
Yok
Güvenilmez — çerezler sahte olabilir
Sunucu tarafında asla kullanma
getClaims()
Nadiren (önbelleğe alınmış JWKs)
JWT-doğrulanmış, DB-doğrulanmamış
Çoğu sunucu okuma
getUser()
Her zaman
DB-doğrulanmış, yetkili
Güvenlik açısından kritik kontroller
// ❌ getSession()'ı sunucu tarafında asla kullanmaconst { data: { session } } = await supabase.auth.getSession()// session.user çerezlerden geliyor — sahte olabilir// ✅ Çoğu sunucu tarafı auth kontrolü için getClaims() kullan// Asimetrik anahtarlarla: JWKs üzerinden yerel JWT doğrulaması (önbelleğe alınmış, hızlı)// Simetrik anahtarlarla: hâlâ Auth sunucusuna istek atar (getUser() ile aynı)const { data, error } = await supabase.auth.getClaims()if (error || !data) redirect('/giris')const kullaniciId = data.claims.sub// ✅ Garantili tazelik gerektiğinde getUser() kullan// Her zaman Auth sunucusuna gider — oturumun iptal edilmediğini doğrularconst { data: { user }, error } = await supabase.auth.getUser()// Kullanım: çıkış tespiti, yasaklı kullanıcılar, şifre değişiklikleri
getClaims() içindeki güvenlik boşluğu: JWT imzasını ve süresini doğrular ama token verildikten sonra kullanıcının yasaklandığını, silindiğini veya sunucu tarafında oturumunun kapatıldığını kontrol etmez. Uygulamanın anında iptal gerektirmesi gerekiyorsa getUser() kullan veya JWT süresini kısa tut (varsayılan 1 saattir).
user_metadata (raw_user_meta_data)
├── Kullanıcı tarafından düzenlenebilir (supabase.auth.updateUser() ile)
├── JWT'de user_metadata olarak görünür
└── Yetkilendirme kararları için ASLA kullanma
app_metadata (raw_app_meta_data)
├── Yalnızca service_role / Admin API ile yazılabilir
├── JWT'de app_metadata olarak görünür
└── Yetkilendirme kararları için GÜVENLİ
// ❌ user_metadata'yı auth kararlarında kullanmaconst rol = claims.user_metadata?.role // kullanıcı bunu 'admin' olarak ayarlayabilir!// ✅ Roller için app_metadata kullanconst rol = claims.app_metadata?.role // yalnızca backend'in yazabildiği// app_metadata ayarlamak Admin API gerektirir (service_role)const { error } = await supabaseAdmin.auth.admin.updateUserById(kullaniciId, { app_metadata: { role: 'admin' },})
-- RLS'i etkinleştiralter table public.siparisler enable row level security;-- Tablo sahipleri için bile RLS'i zorla (güvenlik açısından kritik)alter table public.siparisler force row level security;
public şemasındaki tablolar PostgREST (otomatik oluşturulan REST API) üzerinden erişilebilir. RLS olmadan, yayınlanabilir anahtarınla herkes tüm satırları okuyabilir.
-- Kullanıcılar yalnızca kendi satırlarını görürcreate policy "kullanicilar_kendi_satirlari" on public.siparisler for all to authenticated using (user_id = auth.uid());-- Ayrı select/insert/update policy'leri — ayrıntılı kontrol içincreate policy "kullanicilar_secebilir" on public.siparisler for select to authenticated using (user_id = auth.uid());create policy "kullanicilar_ekleyebilir" on public.siparisler for insert to authenticated with check (user_id = auth.uid()); -- yazma için with checkcreate policy "kullanicilar_kendi_gunceller" on public.siparisler for update to authenticated using (user_id = auth.uid()) -- satır kullanıcıya ait olmalı with check (user_id = auth.uid()); -- güncellenen satır hâlâ kullanıcıya ait olmalı-- Herkese açık okuma, kimlik doğrulanmış yazmacreate policy "acik_okuma" on public.yazilar for select using (true);create policy "auth_yazma" on public.yazilar for insert to authenticated with check (yazar_id = auth.uid());
-- Her kullanıcı bir kiracıya aitcreate table public.kiracı_uyeler ( kiracı_id uuid not null references public.kiracılar(id), user_id uuid not null references auth.users(id), rol text not null default 'uye', primary key (kiracı_id, user_id));-- Yardımcı fonksiyon — SECURITY DEFINER olarak çalışır, RLS'i atlarcreate or replace function public.kullanici_kiracı_idlerini_al()returns setof uuidlanguage sqlsecurity definerset search_path = ''stableas $$ select kiracı_id from public.kiracı_uyeler where user_id = (select auth.uid())$$;-- Policy: kullanıcılar ait oldukları herhangi bir kiracının satırlarını görürcreate policy "kiracı_veri_erisimi" on public.siparisler for all to authenticated using (kiracı_id in (select public.kullanici_kiracı_idlerini_al()));
JWT tabanlı policy'ler için rolleri app_metadata'da sakla:
-- Admin API ile rol ayarla (yalnızca sunucu tarafı)-- supabase.auth.admin.updateUserById(id, { app_metadata: { role: 'admin' } })-- JWT tabanlı rol policy'sicreate policy "admin_tam_erisim" on public.siparisler for all to authenticated using ( (auth.jwt() -> 'app_metadata' ->> 'role') = 'admin' );-- Birleşik: adminler hepsini görür, kullanıcılar kendininkinicreate policy "siparis_erisimi" on public.siparisler for select to authenticated using ( user_id = auth.uid() or (auth.jwt() -> 'app_metadata' ->> 'role') = 'admin' );
En yaygın RLS performans hatası, fonksiyonları önbelleksiz çağırmak:
-- ❌ auth.uid() HER satır için çağrılırcreate policy "yavas_policy" on public.siparisler using (user_id = auth.uid());-- ✅ SELECT içine sar — bir kez değerlendirilir, sonuç önbelleğe alınırcreate policy "hizli_policy" on public.siparisler using (user_id = (select auth.uid()));-- ❌ Alt sorgu satır başına çalışırcreate policy "yavas_takim_policy" on public.siparisler using ( takim_id in ( select takim_id from public.takim_uyeler where user_id = auth.uid() ) );-- ✅ Security definer fonksiyon kullan — bir kez çalışırcreate policy "hizli_takim_policy" on public.siparisler using (takim_id in (select public.kullanici_takim_idlerini_al()));
RLS policy'lerinde kullanılan sütunları her zaman indeksle:
create index siparisler_user_id_idx on public.siparisler (user_id);create index siparisler_kiracı_id_idx on public.siparisler (kiracı_id);create index takim_uyeler_user_id_idx on public.takim_uyeler (user_id);
-- ❌ Bu view TÜM satırları açığa çıkarır — RLS'i yoksayarcreate view siparis_ozetleri as select id, user_id, toplam from public.siparisler;-- ✅ Postgres 15+: security_invoker view'ı RLS'e uymaya zorlarcreate view siparis_ozetleri with (security_invoker = true) as select id, user_id, toplam from public.siparisler;-- Eski Postgres için: doğrudan erişimi iptal etrevoke all on siparis_ozetleri from anon, authenticated;
Bu sessiz bir başarısızlıktır — hata yok, satırlar sadece güncellenmez:
-- ❌ Yalnızca UPDATE policy — güncellemeler sessizce 0 satır dönercreate policy "kendi_guncelle" on public.siparisler for update using (user_id = auth.uid());-- ✅ UPDATE'in çalışması için SELECT policy de olmalıcreate policy "kendi_sec" on public.siparisler for select using (user_id = auth.uid());create policy "kendi_guncelle" on public.siparisler for update using (user_id = auth.uid()) with check (user_id = auth.uid());
-- ✅ Doğru tiplercreate table public.kullanicilar ( id bigint generated always as identity primary key, eposta text not null unique, -- varchar(255) değil, text olusturuldu timestamptz not null default now(), -- her zaman timestamptz aktif_mi boolean not null default true, -- varchar değil fiyat numeric(10, 2), -- float değil (hassasiyet önemli) meta_veri jsonb -- json değil (indekslenebilir, hızlı));-- Neden varchar yerine text?-- text ve varchar Postgres'te özdeş performansa sahip-- varchar(n) kısıtlama ekler ama depolama tasarrufu sağlamaz-- Gerçekten DB seviyesinde uzunluk kısıtı gerekmiyorsa text kullan
Postgres, yabancı anahtar sütunlarını otomatik indekslemez:
create table public.siparisler ( id bigint generated always as identity primary key, user_id uuid not null references auth.users(id) on delete cascade, kiracı_id uuid not null references public.kiracılar(id), durum text not null default 'bekliyor', olusturuldu timestamptz not null default now());-- FK sütunlarını her zaman manuel indekslecreate index siparisler_user_id_idx on public.siparisler (user_id);create index siparisler_kiracı_id_idx on public.siparisler (kiracı_id);-- Eksik tüm FK indekslerini bulselect conrelid::regclass as tablo_adi, a.attname as fk_sutunufrom pg_constraint cjoin pg_attribute a on a.attrelid = c.conrelid and a.attnum = any(c.conkey)where c.contype = 'f' and not exists ( select 1 from pg_index i where i.indrelid = c.conrelid and a.attnum = any(i.indkey) );
-- ✅ küçük harf snake_case — tırnak işareti olmadan her yerde çalışırcreate table public.kullanici_profilleri ( user_id uuid, ad text, soyad text);-- ❌ camelCase veya PascalCase — sonsuza kadar tırnak gerektircreate table public."KullaniciProfilleri" ( -- her zaman tırnak gerekir "userId" uuid, "firstName" text);-- İpucu: ORM'den gelen mixed-case ile karşılaşırsan, uyumluluk katmanı olarak view oluşturcreate view kullanici_profilleri as select "userId" as user_id, "firstName" as ad from "KullaniciProfilleri";
PostgreSQL ADD CONSTRAINT IF NOT EXISTS desteklemez:
-- ❌ Sözdizimi hatasıalter table public.profiller add constraint if not exists profiller_eposta_unique unique (eposta);-- ✅ İdempotent kısıtlama oluşturma için DO bloğudo $$begin if not exists ( select 1 from pg_constraint where conname = 'profiller_eposta_unique' and conrelid = 'public.profiller'::regclass ) then alter table public.profiller add constraint profiller_eposta_unique unique (eposta); end if;end $$;
-- 100M+ satır veya zaman serisi verisi içincreate table public.olaylar ( id bigint generated always as identity, olusturuldu timestamptz not null, veri jsonb) partition by range (olusturuldu);create table public.olaylar_2025_01 partition of public.olaylar for values from ('2025-01-01') to ('2025-02-01');create table public.olaylar_2025_02 partition of public.olaylar for values from ('2025-02-01') to ('2025-03-01');-- Eski veriyi anında sil (saatler süren DELETE yerine)drop table public.olaylar_2024_01;
create table public.profiller ( id uuid primary key references auth.users(id) on delete cascade, tam_ad text, avatar_url text, guncellendi timestamptz default now());alter table public.profiller enable row level security;create policy "profiller_kendi_sec" on public.profiller for select to authenticated using (id = (select auth.uid()));create policy "profiller_kendi_guncelle" on public.profiller for update to authenticated using (id = (select auth.uid())) with check (id = (select auth.uid()));-- Kayıtta otomatik profil oluşturcreate or replace function public.yeni_kullanici_isle()returns triggerlanguage plpgsqlsecurity definerset search_path = ''as $$begin insert into public.profiller (id, tam_ad) values (new.id, new.raw_user_meta_data ->> 'full_name'); return new;end;$$;create trigger auth_kullanici_olusturuldu after insert on auth.users for each row execute procedure public.yeni_kullanici_isle();
-- B-tree (varsayılan): =, <, >, BETWEEN, IN, LIKE 'önek%', IS NULLcreate index siparisler_olusturuldu_idx on public.siparisler (olusturuldu);-- GIN: diziler, JSONB içerme (@>), tam metin aramacreate index urunler_ozellikler_gin_idx on public.urunler using gin (ozellikler);create index yazilar_icerik_fts_idx on public.yazilar using gin (to_tsvector('turkish', icerik));-- GiST: geometrik veri, aralık türleri, en yakın komşu (KNN)create index konumlar_nokta_idx on public.yerler using gist (konum);-- BRIN: çok büyük zaman serisi tablolar (B-tree'den 10-100x küçük)-- Yalnızca veri sütuna göre fiziksel olarak sıralıyken faydalıcreate index olaylar_zaman_brin_idx on public.olaylar using brin (olusturuldu);-- Hash: yalnızca eşitlik (= operatörü), eşitlik için B-tree'den biraz hızlıcreate index oturumlar_token_hash_idx on public.oturumlar using hash (token);
Sütun sırası önemli. Önce eşitlik sütunları, sonra aralık sütunları:
-- Sorgu: WHERE durum = 'bekliyor' AND olusturuldu > '2025-01-01'-- ✅ Doğru sıra: eşitlik aralıktan öncecreate index siparisler_durum_olusturuldu_idx on public.siparisler (durum, olusturuldu);-- Bu indeks şunları yanıtlayabilir:-- WHERE durum = 'bekliyor'-- WHERE durum = 'bekliyor' AND olusturuldu > '2025-01-01'-- Bu indeks verimli yanıtlayamaz:-- WHERE olusturuldu > '2025-01-01' (en sol önek yok)
Sık seçilen sütunları dahil ederek heap erişimini önle:
-- Normal: indeks taraması satır ID'sini bulur, sonra heap'ten müşteri_id, toplam alırcreate index siparisler_durum_idx on public.siparisler (durum);select durum, musteri_id, toplam from public.siparisler where durum = 'gonderildi';-- Kapsayan: tüm sütunlar indeksten gelir, heap erişimi yokcreate index siparisler_durum_kapsayan_idx on public.siparisler (durum) include (musteri_id, toplam);
Yalnızca gerçekten sorguladığın satırları indeksle:
-- Yalnızca aktif kullanıcıları indeksle (sorguların %90'ı deleted_at is null filtreler)create index kullanicilar_aktif_eposta_idx on public.kullanicilar (eposta) where deleted_at is null;-- Yalnızca bekleyen siparişler (tamamlananlar nadiren sorgulanır)create index siparisler_bekliyor_idx on public.siparisler (olusturuldu) where durum = 'bekliyor';-- Yalnızca null olmayan SKU'larcreate index urunler_sku_idx on public.urunler (sku) where sku is not null;
Edge Functions Deno üzerinde edge'de çalışır. Şunlar için kullan:
✅ Webhook'lar (Stripe, GitHub, SendGrid) — HMAC imza doğrulaması gerekir
✅ API anahtarlarını gizlemenin gerektiği üçüncü taraf API çağrıları
✅ Özel auth akışları (sihirli linkler, özel OAuth)
✅ Görüntü/dosya işleme
✅ Resend/Postmark ile e-posta gönderme
✅ Next.js sunucunda istemediğin ağır hesaplamalar
❌ Basit CRUD — PostgREST (otomatik oluşturulan API) ile doğrudan yapabilirsin
❌ Server Action olabilecek iş mantığı — Next.js kullan
❌ Uzun süren işler (> 150s zaman aşımı) — pg_cron veya harici kuyruklar kullan
# Tüm fonksiyonları yerel olarak sunsupabase functions serve# Belirli bir fonksiyonu env ile sunsupabase functions serve eposta-gonder --env-file .env.local# Deploy etsupabase functions deploy eposta-gonder
✅ Canlı işbirliği özellikleri (varlık, imleçler)
✅ Sohbet / mesajlaşma
✅ Yenileme olmadan güncellenen panel metrikleri
✅ Bildirim iletimi
✅ Sipariş durumu takibi
❌ 30 saniyede bir değişen veriler — polling yeterli
❌ Her tablo — yalnızca gerekli yerde etkinleştir
❌ Yüksek hacimli tablolar — Postgres Changes yerine Broadcast kullan
Storage, aynı RLS sistemini kullanır ama storage.objects üzerinde:
-- Kullanıcılar kendi klasörlerine yükleyebilircreate policy "kullanicilar_kendi_yukler" on storage.objects for insert to authenticated with check ( bucket_id = 'avatarlar' and (storage.foldername(name))[1] = auth.uid()::text );-- Kullanıcılar kendi dosyalarını okuyabilircreate policy "kullanicilar_kendi_okur" on storage.objects for select to authenticated using ( bucket_id = 'belgeler' and (storage.foldername(name))[1] = auth.uid()::text );-- Public bucket için herkese açık okumacreate policy "acik_okuma" on storage.objects for select using (bucket_id = 'avatarlar');-- ÖNEMLİ: Upsert (dosya değiştirme) INSERT + SELECT + UPDATE gerektirircreate policy "kullanicilar_kendi_upsert" on storage.objects for update to authenticated using ((storage.foldername(name))[1] = auth.uid()::text) with check ((storage.foldername(name))[1] = auth.uid()::text);
# Yerel Supabase'i başlatsupabase start# Yeni migration dosyası oluştursupabase migration new siparisler_tablosu_ekle# Şema üzerinde yinele — execute_sql veya psql kullan, apply_migration değil# apply_migration her çağrıda geçmiş yazar, diff'i imkansız kılar# Şema hazır olduğunda diff'ten migration üretsupabase db pull --local --yes# Migration'ı uygulasupabase db push --local# Migration listesini kontrol etsupabase migration list
-- supabase/migrations/20250420120000_siparisler_tablosu_ekle.sql-- Yıkıcı işlemleri her zaman transaction içine sarbegin;create table public.siparisler ( id bigint generated always as identity primary key, user_id uuid not null references auth.users(id) on delete cascade, durum text not null default 'bekliyor', toplam numeric(10, 2) not null, olusturuldu timestamptz not null default now(), guncellendi timestamptz not null default now());create index siparisler_user_id_idx on public.siparisler (user_id);create index siparisler_durum_idx on public.siparisler (durum);create index siparisler_olusturuldu_idx on public.siparisler (olusturuldu desc);alter table public.siparisler enable row level security;create policy "siparisler_kullanici_policy" on public.siparisler for all to authenticated using (user_id = (select auth.uid())) with check (user_id = (select auth.uid()));commit;
-- ✅ Güvenli: nullable sütun ekleme (tablo yeniden yazma yok)alter table public.kullanicilar add column biyografi text;-- ✅ Güvenli: varsayılan değerli sütun ekleme (Postgres 11+)alter table public.kullanicilar add column premium_mi boolean not null default false;-- ⚠️ Dikkatli: mevcut sütuna NOT NULL ekleme-- Önce nullable ekle, doldurandır, sonra kısıtlamayı eklealter table public.kullanicilar add column telefon text;update public.kullanicilar set telefon = '' where telefon is null;alter table public.kullanicilar alter column telefon set not null;-- ⚠️ Dikkatli: büyük tabloya indeks ekleme-- Tablo kilidini önlemek için CONCURRENTLY kullancreate index concurrently siparisler_kiracı_id_idx on public.siparisler (kiracı_id);-- ❌ Tehlikeli: tüm kullanımları kontrol etmeden sütun düşürme-- Önce kontrol et: grep -r 'eski_sutun_adi' kod-tabaninalter table public.kullanicilar drop column eski_sutun;
-- Her zaman buffers kullan — önbellek isabet oranını gösterirexplain (analyze, buffers, format text)select * from public.siparislerwhere user_id = 'abc' and durum = 'bekliyor';-- Dikkat edilecek başlıca şeyler:-- "Seq Scan" büyük tabloda → eksik indeks-- "Rows Removed by Filter: 999000" → düşük seçicilik-- "Buffers: read >> hit" → veri önbellekte değil-- Sort'ta "external merge" → work_mem'i artır-- "Nested Loop with loops=10000" → farklı join stratejisi düşün
-- Etkinleştir (Supabase'de genellikle zaten etkin)create extension if not exists pg_stat_statements;-- Toplam süreye göre en yavaş sorgularselect calls, round(total_exec_time::numeric, 2) as toplam_ms, round(mean_exec_time::numeric, 2) as ortalama_ms, round(stddev_exec_time::numeric, 2) as sapma_ms, queryfrom pg_stat_statementsorder by total_exec_time desclimit 20;-- Ortalama > 100ms olan sorgularselect query, mean_exec_time, callsfrom pg_stat_statementswhere mean_exec_time > 100order by mean_exec_time desc;-- Optimizasyon sonrası temiz taban çizgisi için sıfırlaselect pg_stat_statements_reset();
-- Tabloların en son ne zaman analiz edildiğini kontrol etselect relname, last_vacuum, last_autovacuum, last_analyze, last_autoanalyze, n_live_tup, n_dead_tupfrom pg_stat_user_tablesorder by n_dead_tup desc;-- Büyük veri yüklemelerinden sonra manuel analizanalyze public.siparisler;-- Yoğun tablolar için autovacuum ayarlamaalter table public.siparisler set ( autovacuum_vacuum_scale_factor = 0.05, -- %5 ölü tuplelda vacuum (varsayılan %20) autovacuum_analyze_scale_factor = 0.02 -- %2 değişimde analiz (varsayılan %10));
Supabase varsayılan olarak transaction modunda PgBouncer kullanır. Serverless ortamlarda havuzlayıcı bağlantı dizesini (port 6543), migration'lar için doğrudan bağlantıyı (port 5432) kullan:
# Havuzlayıcı — üretim uygulamasında kullan (serverless, edge)DATABASE_URL=postgresql://postgres.ref:[sifre]@aws-0-eu-central-1.pooler.supabase.com:6543/postgres# Doğrudan — migration'lar ve CLI işlemleri için kullanDATABASE_URL=postgresql://postgres:[sifre]@db.ref.supabase.co:5432/postgres
-- ❌ Harici çağrılarla uzun transaction — kilitleri tutarbegin;select * from siparisler where id = 1 for update;-- ... ödeme API'sine HTTP çağrısı (2-5 saniye) ...update siparisler set durum = 'odendi' where id = 1;commit;-- ✅ Harici işi transaction dışında yap-- Adım 1: doğrula, API'yi transaction dışında çağır-- sonuc = await odemeAPI.tah(...) -- Adım 2: atomik güncellemebegin;update siparisler set durum = 'odendi', odeme_id = $1 where id = $2 and durum = 'bekliyor' returning *;commit;
-- Birden fazla worker, birbirini bloklamadanupdate public.islerset durum = 'isleniyor', worker_id = $1, baslangic = now()where id = ( select id from public.isler where durum = 'bekliyor' order by olusturuldu limit 1 for update skip locked)returning *;
☐ NEXT_PUBLIC_ değişkenleri yalnızca publishable key içeriyor — service_role/secret asla
☐ Service role anahtarı yalnızca sunucu tarafı env değişkenlerinde (NEXT_PUBLIC_ ön eki yok)
☐ Açığa çıktıysa anahtarları döndür (Dashboard → API Settings)
☐ getSession() sunucu tarafında asla kullanılmıyor — getClaims() veya getUser() kullan
☐ Yetkilendirme için user_metadata asla kullanılmıyor — app_metadata kullan
☐ app_metadata yalnızca Admin API ile ayarlanıyor (service_role), istemciden asla
☐ JWT süresi güvenlik gereksinimlerine uygun ayarlanmış
☐ Kullanıcı silme mevcut token'ları iptal ETMİYOR — önce çıkış yaptır veya süresini bekle
☐ Public şemasındaki her tabloda RLS etkin
☐ Hassas tablolarda force row level security ayarlı
☐ RLS fonksiyonları performans için SELECT içinde sarılmış: (select auth.uid())
☐ RLS policy'lerinde kullanılan her sütunda indeks var
☐ View'lar security_invoker = true kullanıyor (Postgres 15+) veya erişim iptal edilmiş
☐ security definer fonksiyonları açık olmayan şemada (public değil)
☐ UPDATE policy'lerinin eşleşen SELECT policy'si var
☐ Uygulamada superuser kimlik bilgisi yok
☐ Uygulama rolünün minimum gerekli izinleri var
☐ Migration'lar güvenlik açısından incelendi (büyük tablolarda indeksler için CONCURRENTLY)
☐ Sorgu izleme için pg_stat_statements etkin
Herkese açık okuma, yazma kısıtı yok
→ create policy "..." using (true)
Kullanıcı satıra sahip
→ using (user_id = (select auth.uid()))
Kiracı izolasyonu
→ using (kiracı_id in (select kullanici_kiracı_idlerini_al()))
Rol tabanlı (app_metadata'dan)
→ using ((auth.jwt() -> 'app_metadata' ->> 'role') = 'admin')
Birleşik kullanıcı + admin
→ using (user_id = (select auth.uid()) OR (auth.jwt() -> 'app_metadata' ->> 'role') = 'admin')
Güvenli (kilit yok):
ALTER TABLE ... ADD COLUMN (nullable veya varsayılan değerli)
CREATE INDEX CONCURRENTLY
CREATE POLICY
Dikkatli (kısa kilit):
ALTER TABLE ... ADD COLUMN NOT NULL (önce doldur)
ALTER TABLE ... ALTER COLUMN TYPE
Tehlikeli (tam tablo kilidi):
CREATE INDEX (CONCURRENTLY olmadan)
ALTER TABLE ... DROP COLUMN
VACUUM FULL
Son güncelleme: Nisan 2026. @supabase/ssr son sürüm, asimetrik JWT anahtarları (Mayıs 2025+'tan varsayılan), yeni API anahtarı formatı (sb_publishable_xxx / sb_secret_xxx) kapsar.