Mühendislik

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.

Yazar Berke Erdoğan27 Mayıs 20269 dk okuma
Multi-tenancy olmadan B2B SaaS yetkilendirmesi — Moditra hikâyesi

Ö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.
  • curl ile 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.

İlgili VakaModitra — B2B Modelist Atölye SaaS PlatformuModitra · SaaS Platformu / UI / UX · 2026Vaka çalışmasını oku →

İ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.