Multi-tenancy olmadan B2B SaaS yetkilendirmesi — Moditra hikâyesi
Her B2B SaaS şablonu önce multi-tenancy seç der. Moditra'da seçmedik. Bunun yerine ne kurduk — role-based access, service-layer authorization boundary, state-machine iş akışı, SSE — ve neden tutmaya devam ediyor.

ÖzetÇoğu B2B SaaS şablonu multi-tenancy ile başlar. Moditra başlamadı. Bu yazı role-based access + service-layer authorization'ın production'da nasıl göründüğünü anlatıyor — kabul ettiğimiz trade-off, zorladığı mimari kararlar ve bu kısayolun ne zaman çalışmayı bırakacağı.
Her B2B SaaS eğitimi aynı şekilde başlar: "Önce multi-tenancy stratejini seç. Subdomain per tenant mı, path-based mi? Row-level security mi, schema-per-tenant mı?" Bu seçim, geri kalan her şeyi kısıtlayan yük-taşıyan karar olarak ele alınır.
Biz almadık.
Moditra modelist atölyeleri için bir B2B platformu
— moda tasarımlarını üretime hazır pattern'lere çeviren stüdyolar. İlk
sürümü üç kullanıcı rolü, tek PostgreSQL veritabanı ve sıfır workspace ile
canlıya çıkardık. Subdomain yok. tenant_id kolonu yok. Multi-tenant
middleware yok. Bu yazı, mimari muhakeme, kabul ettiğimiz trade-off ve kod
tabanının altında ne olduğunu anlatıyor.
Geleneksel pitch — ve uyduğu yer
Workspace multi-tenancy şu durumlarda mantıklıdır:
- Müşteriler veri izolasyonunu sözleşmenin parçası olarak bekler (B2B enterprise satış).
- Tenant'lar hiyerarşik üyeleri olan organizasyonlardır.
- İleride per-tenant konfigürasyon, branding veya faturalandırma gerekecektir.
- Yatay büyüyorsun — aynı ürün, çok şirket.
Bunlar yoksa, maliyet gerçek, fayda teorik. Her sorgu bir tenant filtresi taşır. Her UI route'u tenant'ı bilir. Her test tenant fixture'larını seed'ler. Kazancı kazanmadan bütün bedeli ödersin.
Moditra'nın gerçeği: her kullanıcı ya modelist sipariş veren bir müşteri, ya o siparişler üzerinde çalışan bir modelist, ya da platformu yöneten bir admin. Müşteriler organizasyon kurmuyor. Modelistlerin altında ekip yok. Modellenecek hiyerarşik bir "workspace" yok — sadece rolleri olan bireyler.
Workspace değil, rol
Kullanıcı şeması küçük. Belirleyici alan üç değerli bir rol enum'u: sipariş veren alıcı, sipariş üzerinde çalışan operatör ve platform yöneticisi. Bu tek alan her şeyi kapı tutar. Alıcılar verdikleri siparişleri görür. Operatörler kendilerine atanan siparişleri görür. Yöneticiler tüm platformu görür. Kaynak sahipliği sorgu zamanında dayatılır — alıcı sorguları için buyer-id filtresi, operatör sorguları için assignee-id filtresi — ama sınır kullanıcının satırı, workspace'in satırı değil.
Müşteriler ekip yönetseydi bu ölçeklenmezdi. Yönetmiyorlar. Yönetirlerse,
göç sınırlı: user'lara organizationId ekle, sipariş servisine
organization-aware sorgular ekle, UI'ya workspace switcher ekle. O bedeli
gerektiğini kanıtlayan veri varken ödemeyi tercih ederiz, spekülatif
olarak inşa etmek yerine.
Service layer, authorization sınırıdır
Moditra'nın mimarisi burada görüş bildiriyor. Tüm business logic tek bir
iç pakette yaşar: @moditra/services. Panel server action'ları ve
Fastify route'ları bilinçli olarak ince — yirmi satırın altında — çünkü
işleri sadece bir isteği almak, actor'ın user ID'sini çıkarmak ve servisi
çağırmak.
Authorization check'i servis yapar. @moditra/services'deki her fonksiyon
ilk argüman olarak actorUserId alır. Sipariş listelemek, veritabanına
dokunmadan önce servis içinde rol ve sahiplik kontrollerini çalıştırır.
Aktör kimliği iş içinde threadlenmeden business logic'in çalıştığı bir
senaryo yok.
Bu disiplin estetik için değil. Authorization açıkları, B2B'deki en pahalı bug sınıfı. Kontrolü route'a koymak "her seferinde kontrol etmeyi unutma" demek. Kontrolü servise koymak "kontrol fonksiyondur" demek. Yeni bir route handler yanlışlıkla enforcement'ı atlayamaz çünkü çağırdığı servis zaten zorunlu kılıyor.
Stateful akışlar için state machine
Siparişler altı statü arasında hareket eder — taslak, inceleme, ilerleme, onay, tamamlama ve iptal yolu. Her geçiş, rol başına allowed-transitions haritası tarafından kapı tutulur.
Alıcı bir siparişi bir adım ileri taşıyabilir ama tek başına tamamlanmış
işaretleyemez. Operatör inceleme aşamasından devralıp ileri taşıyabilir
ama tek başına iptal edemez. Yönetici her geçişi override edebilir.
Otorite haritadır — kod tabanına serpilmiş
if (status === 'X' && user.role === 'Y') yoktur.
Bu, XState veya Rails state_machine gem'iyle aynı pattern, bir tutam
TypeScript ile yazılmış. Kazanç araç değil — workflow kurallarını,
uygulamanın bir düzine koşullu kontrolünden çıkan bir davranış olarak
değil, tek bir dosyadan sorgulanabilir bir artifact yapmak.
SSE, WebSocket değil
Moditra real-time. Siparişler güncellenir, chat mesajları gelir, notification'lar tetiklenir. WebSocket kullanmıyoruz. Küçük bir Server-Sent Events endpoint kümesi — her real-time domain için tek stream (sipariş güncellemeleri, chat, notification'lar) — olayları client'lara düz HTTP uzun-yaşamlı istekler üzerinden push'lar, proxy'lerin idle bağlantıları timeout etmesini engellemek için 25 saniyelik heartbeat ile.
WebSocket, SaaS real-time için varsayılan cevap. SSE, şu durumlarda doğru cevap:
- Trafik server → client (mesajlar normal POST ile gönderiliyor).
- Mevcut HTTP altyapını miras almak istiyorsun: auth cookie, CDN, proxy, load balancer hepsi özel handling olmadan çalışır.
curlile debug etmek istiyorsun.
SSE'yi seçtik çünkü Node sunucusunun önüne WebSocket-uyumlu bir proxy katmanı sokmaya zorlamadan problemi çözdü. 25 saniyelik heartbeat, hatırlamak zorunda olduğun tek operasyonel incelik. Toplam real-time altyapısı: üç route'a yayılmış 200 satırın altında.
Skeleton'lar CSS, state değil
Loading state'leri React state'ine ulaşmıyor. Tek bir CSS keyframe — 1.4
saniyelik linear shimmer döngüsü — uygulamadaki her skeleton bloğunu
sürer. Component'ler stateless, <SkeletonKpiStrip /> veya
<SkeletonFormCard rows={3} /> gibi isimlendirilmiş. Server-side render
olur, loading state için sıfır JavaScript ship eder, rendering churn
olmadan CSS ile animate olur.
Bu mikrosaniyelerle ölçülen bir performans kazancı değil. Bir kategori
değişimi: useState('loading') yazmıyoruz çünkü component'in üstündeki
Suspense sınırı skeleton'u render eder, sonra streamed içerik onu
değiştirir. Loading state, uygulamanın yönettiği bir state değil —
uygulamanın içinden geçtiği bir layout.
Çapraz-ortam cookie disiplini
Daha az ilginç ama bahsetmeye değer çünkü bir haftalık kafa karışıklığına
mal oldu: production ve staging kök domain'i paylaşıyor, dolayısıyla
güvenli session cookie'si varsayılan olarak ortamlar arasında çakışıyor.
Env-driven bir cookie ismi prefix'i ile çözdük — staging cookie'leri
isminde -staging son eki taşıyor — ve middleware her iki formu da
probe ederek local dev (prefix yok) hâlâ çalışsın diye.
Staging'i staging.app.example.com ve prod'u app.example.com üzerinde
çalıştıran her B2B SaaS sonunda buna çarpar. Düzeltme mekanik. İlk
bulduğunda maliyeti iki gün. Bilmeye değer.
Yeniden yapsak ne değiştirirdik
Moditra'yı bugünkü bilgimizle yeniden kursak:
- Role-based + service-layer authorization: koru. Problem için doğru.
- SSE: koru. Hafif yabancılık, değer.
- Tek PostgreSQL: koru. Cache katmanları sokma baskısı sürekli. İhtiyacın olduğunu kanıtlayana kadar diren.
Bu ne zaman çalışmamaya başlar
Bu mimari, rol seti sabit ve veri sahipliği bireysel olduğu için çalışıyor. Bunlardan herhangi biri değiştiği an — müşteri atölyeler ekip büyütür, white-label tenant'lar ortaya çıkar, regülasyonel veri izolasyonu sözleşme haline gelir — role-based kısayol kırılır ve düzgün workspace multi-tenancy'ye geçeriz.
O ana kadar, işi yapan en basit kod doğru koddur. Workspace multi-tenancy varsayılan B2B SaaS mimarisidir. Her zaman doğrusu değildir.
İstanbul'dan çalışan, globalde aktif küçük bir ürün stüdyosuyuz. Bir B2B platform kuruyorsanız ve multi-tenancy varsayılanı gerçeğinize uymuyorsa, bu sevdiğimiz bir konuşma — bize ulaşın veya nasıl çalıştığımızı okuyun.