---
title: "Supabase Row Level Security ile Yetkilendirme: Kim Neyi Görsün?"
description: "Çok şubeli bir uygulamada her kullanıcının sadece kendi verisini görmesini nasıl sağlarsınız? Supabase RLS ile veritabanı seviyesinde güvenlik rehberi."
date: 2026-04-08
category: guvenlik
tags: ["supabase", "rls", "postgresql", "güvenlik", "yetkilendirme"]
url: https://mikroerp.dev/blog/supabase-rls-cok-katmanli-yetkilendirme/
---

## Sorun: Frontend Kontrolü Yeterli Değil

Çok şubeli bir uygulama geliştirdiniz. Adana şubesi sadece Adana verilerini, Merkez sadece kendi verilerini görmeli. Frontend'te if kontrolü yaptınız:

```javascript
// ❌ Güvenli DEĞİL
if (user.role === 'admin') {
  // tüm veriyi göster
} else {
  // sadece kendi şubesini göster
}
```

Sorun: Bir kullanıcı tarayıcının geliştirici konsolunu açar, API'yi doğrudan çağırır ve **tüm şubelerin verisini** çeker. Frontend kontrolü sadece UX içindir, güvenlik değildir.

**Çözüm**: Güvenliği **veritabanı seviyesinde** sağlamak. PostgreSQL'in Row Level Security (RLS) özelliği tam bunu yapar.

## RLS Nedir?

RLS, veritabanının kendisinin "bu kullanıcı bu satırı görebilir mi?" diye kontrol etmesidir. Nasıl çağrılırsa çağrılsın — frontend, API, konsol — güvenlik çalışır.

```
Frontend kontrolü:  if (isAdmin) → göster
                    ↓ Bypass edilebilir ❌

RLS kontrolü:       PostgreSQL → "Bu kullanıcının şubesi bu satırla eşleşiyor mu?"
                    ↓ Bypass edilemez ✅
```

## Temel Kullanım

### 1. RLS'i Etkinleştirme

```sql
-- Tabloda RLS'i aç
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
```

Bu komuttan sonra, tablo artık **hiç kimseye** veri göstermez (admin rolleri hariç). Erişim için policy (kural) yazmanız gerekir.

### 2. Okuma Kuralı

```sql
-- Tüm giriş yapmış kullanıcılar ürünleri görebilir
CREATE POLICY "urunleri_oku" ON products
FOR SELECT TO authenticated
USING (true);
```

### 3. Şube Bazlı Kısıtlama

```sql
-- Kullanıcı sadece yetkili olduğu şubelerin siparişlerini görsün
CREATE POLICY "siparis_sube_filtre" ON orders
FOR SELECT TO authenticated
USING (
  EXISTS (
    SELECT 1 FROM user_roles
    WHERE user_email = auth.jwt() ->> 'email'
    AND (
      role = 'admin'                    -- Admin her şeyi görür
      OR branch = orders.target_branch  -- Diğerleri kendi şubesini
    )
  )
);
```

Bu kural şunu der: "Eğer kullanıcı admin ise her şeyi görsün. Değilse sadece kendi şubesine ait siparişleri görsün."

## Rol Hiyerarşisi Örneği

| Rol | Stok | Satış | Sipariş | Admin |
|-----|:----:|:-----:|:-------:|:-----:|
| Admin | ✅ | ✅ | ✅ | ✅ |
| Yönetici | ✅ | ✅ | ✅ | ❌ |
| Şube Müdürü | ✅ | ✅ | ✅ (kendi şubesi) | ❌ |
| Temsilci | ✅ | 🔒 (kendi) | ❌ | ❌ |

### Yönetici Kontrolü İçin Yardımcı Fonksiyon

```sql
CREATE FUNCTION is_manager_or_admin()
RETURNS BOOLEAN AS $$
BEGIN
  RETURN EXISTS (
    SELECT 1 FROM user_roles
    WHERE user_email = auth.jwt() ->> 'email'
    AND role IN ('admin', 'manager', 'branch_manager')
  );
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

-- Sadece yöneticiler yaşlandırma verisi yükleyebilsin
CREATE POLICY "yaslandirma_yukle" ON customer_aging
FOR INSERT TO authenticated
USING (is_manager_or_admin());
```

## Sık Yapılan Hatalar

### 1. Şube adlarında büyük/küçük harf uyuşmazlığı

```javascript
// ❌ "MERKEZ" ≠ "merkez" — RLS eşleşmez
const hasAccess = allowedBranches.includes(order.target_branch);

// ✅ Her iki değeri de küçük harfe çevirin
const hasAccess = allowedBranches
  .map(b => b.toLowerCase())
  .includes(order.target_branch?.toLowerCase());
```

### 2. RLS'i açtığınız halde policy yazmayı unutmak

Sonuç: Tablo **hiç veri döndürmez**. Kullanıcı "veriler kayboldu" der.

### 3. RPC fonksiyonlarının RLS'i bypass etmesi

SECURITY DEFINER fonksiyonlar RLS'i atlar. [Detaylı yazımıza](/blog/postgresql-security-definer-vs-invoker) bakın.

## Debugging: "Neden Veri Gelmiyor?"

```sql
-- 1. Policy'leri listele
SELECT * FROM pg_policies WHERE tablename = 'orders';

-- 2. Admin olarak veriyi kontrol et
SET ROLE postgres;
SELECT COUNT(*) FROM orders WHERE branch = 'MERKEZ';

-- 3. Normal kullanıcı olarak dene
SET ROLE authenticated;
SELECT COUNT(*) FROM orders WHERE branch = 'MERKEZ';
-- Sonuç farklıysa → RLS engelliyor
```

## Yeni Tablo Ekleme Checklist'i

Her yeni tablo oluşturduğunuzda:

```sql
-- 1. RLS aç
ALTER TABLE yeni_tablo ENABLE ROW LEVEL SECURITY;

-- 2. Okuma kuralı ekle
CREATE POLICY "yeni_tablo_oku" ON yeni_tablo
FOR SELECT TO authenticated USING (true);

-- 3. Gerekirse yazma kuralı ekle
CREATE POLICY "yeni_tablo_yaz" ON yeni_tablo
FOR INSERT TO authenticated USING (is_manager_or_admin());
```

## Özet

- Frontend güvenlik kontrolü **kullanıcı deneyimi** içindir
- Gerçek güvenlik **veritabanında** (RLS) olmalıdır
- RLS bir kez kurulunca, API nasıl çağrılırsa çağrılsın güvenlik sağlanır

Bu yazıdaki yapı çoğu proje için yeterlidir. 9 farklı rol, çoklu şube erişimleri ve modül bazlı yetkilendirme gerektiren daha karmaşık senaryolarda ise mimarinin derinleşmesi gerekir.

---

*Bu yazı [AstaFlow Case Study](/case-study) serisinin bir parçasıdır.*