XYZsys
XYZsys
Tasarım Dokümantasyonu
Kararlar
Hazırlayan Floow
+
+
Kurumsal Yönetim PlatformuXYZsys

Tek backend, binlerce firma.

Tek bir NestJS backend birçok firmaya (tenant) hizmet verir; her firma lisans/modül satın alır, kendi domain'ini bağlar, bir tema seçip özelleştirir, isterse kendi veritabanını getirir (BYODB), ve sitesi canlıya çıkar. İş yetenekleri tek bir modül kontratı üzerinden takılıp çıkarılabilir.

~10k
maks. firma / headroom
6
bağımsız uygulama
26
control DB tablosu
1 = 1
firma = postgres DB
ÖLÇÜLDÜ · 3000 LAUNCH + 10K HEADROOM · FEASIBLE
Pratik Anlatım Teknik bilgi gerektirmez · herkes için

Genel bakış : Şuan ne yapıyoruz?

Tek cümleyle

Tek bir merkezden yönetilen, ama içinde binlerce firmanın birbirinden tamamen bağımsız çalıştığı bir kurumsal yönetim platformunun temelini kuruyoruz.

Tek sistem, binlerce firma
Hepsi tek merkezden yönetilecek; yine de her firma kendi başına, diğerlerinden izole çalışır.
Sadece istediğini öde
Firma yalnızca ihtiyacı olan özellikleri satın alır; kullanmadığı her şey kapalı kalır.
Veriler ayrı kasada
Her firmanın bilgisi kendi ayrı veritabanında saklanır — firmalar birbirinin verisine asla erişemez.
Aynı gün yayında
Kaydol, özelliğini seç, alan adını bağla, temanı seç — siten canlıya çıkar.
Şu an neredeyiz?
Tasarım neredeyse bitti ve tüm kararlar kesinleşti (kilitli). Şuan yazılımı kodlanıyor. Bu belgenin geri kalanı, yukarıda sade dille anlatılan yapının teknik detaylarıdır.
/01

Sistem Haritası

6 bağımsız uygulama, tek backend. Gateway yalnızca frontend seçer + tenant context enjekte eder. İki realm: merkezî Control DB ↔ firma-başına Tenant DB.

Frontend uygulamaları
landing
adminpanel
backoffice
themes
gateaway domain → tenant · imzalı context
backend · NestJS connection registry — PgBouncer + LRU
CONTROL DB
tenants · datasources (şifreli) · ürün katalog · grants · domains · backoffice · audit
TENANT DB
firma başına bir Postgres · firmanın kendi verisi
BYODB
müşterinin kendi PG sunucusunda · sadece PostgreSQL
/02

Belge Haritası

Kontrol Düzlemi02

2 düzlem, multi-DB engine, abonelik/modül/entitlement, gateway, auth, güvenlik (§20), benchmark sonuçları.

Entity Modeli03

Base→sub entity'ler, value object'ler, aggregate'ler, Product STI, IModule plug-in kontratı, ER + UML diyagramları.

Yük Testi04

DB-per-tenant topolojisinin yük testi: kapasite, sınırlar, sizing yasası, provisioning/migration ölçümleri.

Yol Haritası05

Tasarımı koda dökmek için fazlandırma: Faz 0–6, her fazın hedefi/çıktısı/bağımlılığı + dikey-dilim-önce stratejisi.

/03

Kilitli Kararlar

Tüm mimari kararlar kilitli; açık-soru yok. Sıradaki adım kodlama.

{{ d.k }}
{{ d.v }}
/04

Sözlük

{{ g.t }}
{{ g.d }}
02.1 · Kontrol Düzlemi

Mimari & Kapsam

Bu fazda tasarlanan: bir tenant'ın lisans/modül/tema satın alıp, kendi DB'sini bağlayıp, admin panelinden yöneterek sitesinin canlıya çıkması için gereken tüm kontrol-düzlemi makinesi. İş mantığı / org-schema / approval-engine sonraki faz.

Bu fazda tasarlanan
Lisans/modül/tema satın alma · kendi DB'sini bağlama · async provisioning state-machine · modül/tema/gateway aktivasyonu · entitlement gating · "site canlı" durum zinciri.
Sonraki fazlar (hariç)
İş mantığı (Org Schema, Request/Approval/Decision Engine, Audit, 4-katmanlı yetki) · modüllerin iş mantığı · tam şema alan listeleri · frontend implementasyonu.
Stack kararı
NestJS · PostgreSQL · Drizzle ORM

Teklifteki Prisma yerine bilinçli sapma — gerekçe: dinamik multi-DB / BYODB + karmaşık SQL + transaction + AI-fidelity tip sistemi. Mongo elendi (ERP → ağır transaction + ilişki).

Topoloji
Uniform DB-per-tenant

1 firma = 1 Postgres DB; birçok DB az sayıda paylaşımlı cluster'da; BYODB kendi sunucusunda. Canlı routing, sync yok. BYODB sadece PostgreSQL.

3 izolasyon seviyesi

01 FİRMAFiziksel DB sınırı
Tenant = ayrı Postgres veritabanı. tenant_id filtresi YOK — izolasyon CLS'in çözdüğü bağlantıdan gelir.
02 ŞUBEcompany_id · mantıksal
Tenant DB içinde, merkezî helper ile otomatik scope + Postgres RLS (CLS GUC).
03 SON-MÜŞTERİTenant'ın kendi müşterisi — yalnızca bir veri satırı.

Premises (onaylandı)

{{ p.k }}
{{ p.v }}
02.2 · Kontrol Düzlemi

Multi-DB Motoru

Control DB tek bir Drizzle instance'ı; her tenant DB'si istek başına registry'den çözülen ayrı Drizzle instance'ı. İzolasyonun bel kemiği: servise vermeden önce çözülen descriptor'ın tenant_id'si CLS'teki tenant_id ile eşleşmek zorunda.

Datasource descriptor — control DB
tenant_datasource {
tenant_id, kind: managed | byodb, cluster_id?,
host, port, db_name, username,
secret_ref // secret-manager pointer, ssl_mode,
schema_version: { feature_key → applied_migration_id } // per-module,
pool_max,
status: provisioning | active | migrating | unreachable | error,
last_health_at
}

Connection registry + yaşam döngüsü

{{ r.t }}
{{ r.d }}
RAM MODELİ
Maliyet = f(PgBouncer max_server_conns tavanı + app-tarafı küçük pool'lar), tenant sayısının fonksiyonu DEĞİL. Sabit backend tavanı (150–300) → ~1–3GB. Naif yol bu tavanı kaldırdığı için patlar.
Tenant context
nestjs-cls (AsyncLocalStorage). Edge guard tenant_id'yi CLS'e koyar; servisler elle taşımaz; arka plan işleri cls.run({tenantId}, fn) ile sarılır.

Repository / ORM katmanı — R1–R8

{{ r.n }} {{ r.d }}
02.3 · Kontrol Düzlemi

Modül & Entitlement

Sadeleştirici prensip: Module = tek kavram, beş hizalı görünüm. Yeni modül eklemek = tek paket + tek ürün; gating / aktivasyon / UI / migration hepsi türetilir.

{{ f.n }}
{{ f.t }}
{{ f.d }}

Katmanlar — oturmuş patternler

Satıştan aktivasyona, her katman bir sonrakini besler.

{{ l.t }} {{ l.tag }}
{{ l.d }}
Kilit ayrım
"Nasıl satın alındı (kanal)" "neye yetkilisin (entitlement)". Karmaşıklığı çözen budur.

Tek modelden türeyen gereksinimler

{{ m.t }}
{{ m.d }}
MANIFEST
Davranış / deps / şema → kodda manifest (developer-owned). Fiyat / paketleme → DB ürün kataloğu (backoffice-owned). feature_key ile bağlı.
ON-ACTIVATION
Modül şeması: tenant modül satın alınca o modülün migration'ları o tenant DB'sine koşulur; almayan tenant'ta tablolar hiç yoktur. Niche modül için 10k DB migrate edilmez.
DEPS
Deps eksikse aktivasyon bloklanır; satış akışı (backoffice) eksik zorunlu modülleri grant/bundle'a otomatik ekler. Entitlement motoru deps'i zorlar, satış katmanı tamamlar.
02.4 · Kontrol Düzlemi

Provisioning & "Site Canlıya Çıkar"

Aktivasyon türetilir, elle bağlanmaz: bir modül "aktif" iff entitlement set feature_key'i içerir ve deps karşılanmış ve (şema gerekiyorsa) migration'ları tenant DB'ye uygulanmış.

Provisioning state machine

Control plane + worker/outbox sürer; admin panel canlı yansıtır; her adım resumable.

Runtime canlı yolu

{{ p.n }} {{ p.d }}
Domain doğrulama — domain_verifying
Sahiplik DNS TXT kaydı (veya HTTP challenge) ile doğrulanır; doğrulanmadan site canlı olmaz. Herhangi bir halka eksikse uygun ön-durum sayfası.

Migration stratejisi — iki set (control + tenant)

{{ m.t }}
{{ m.d }}
02.5 · Kontrol Düzlemi

Auth & Gateway Güven Sınırı

Gateway → backend imzalı tenant context (spoof'lanabilir düz X-Public-Key yerine). İzolasyonun güvenlik bel kemiği budur.

gateway
özel anahtarla imzalar
Ed25519 / kısa-TTL JWT →
backend
yalnızca public key tutar
payload = { tenant_id, exp (kısa TTL), nonce } + replay koruması + anahtar rotasyonu
İmza geçersiz / expired → istek reddedilir (asla tenant'a düşmez).
Katmanlı guard + jti-refresh
Kanıtlanmış katmanlı guard deseni korunur (yeniden inşa).
+ Redis access-token iptali
Önceki yapıda yoktu; tek kill-switch suspended-cache'di. Artık gerçek revocation.

Caching / Redis — paylaşımlı (process-local LRU yerine)

{{ r.t }}
{{ r.d }}
02.6 · Kontrol Düzlemi

Güvenlik — Kapatma Maddeleri (§20)

Tasarım-seviyesi adversarial güvenlik review'ı (8 lens, kod yok). Posture: fiziksel izolasyon omurgası güçlü; bu maddeler veri-yapıları / kodlama fazından ÖNCE tasarıma girer.

14
P0 · kritik
33
P1 · yüksek
19
P2 · orta
{{ s.sev }} {{ s.t }}
{{ it }}
Tasarımda henüz olmayan yüzeyler (sonra)
custom-domain TLS/ACME suistimali · domain-ownership / subdomain-takeover · tenant offboarding · WebSocket re-auth · enumeration / timing oracle · outbox bütünlüğü · backoffice/adminpanel web-appsec (CSRF/XSS/CSP/IDOR) · auth brute-force · app-level SQLi · insider / break-glass.
02.7 · Kontrol Düzlemi

Benchmark — Topoloji Feasibility

Ortam: OrbStack/Mac, Postgres 16, max_connections=200, shared_buffers=256MB, PgBouncer transaction-pool. Lokal koşum şekil/knee verir; mutlak tavan prod-donanım gerektirir.

{{ e.n }} {{ e.t }}
{{ e.d }}
Sizing Yasası
Eşzamanlı-aktif distinct tenant / cluster ≈ max_connections
PgBouncer havuzları (user,db) başına; cross-db paylaşım yok. Ölçekleme: max_connections yükselt (~500-1000, RAM maliyeti) → sonra cluster'lara shard. 3000 firma @ ~%10-20 tepe-aktif (~300-600) → 1 ayarlı cluster (max_conn~600) veya 2-3×200.
VERDICT
Uniform DB-per-tenant, ölçülen sinyallerde 3000 launch + 10k headroom için feasible. Backup: WAL/fiziksel (pgBackRest) + paralel per-tenant pg_dump, asla seri pg_dumpall. Yeni bulgu: PgBouncer tek-thread → ~10k tx/s'te 1 core'da doyuyor → prod'da çok-pooler (SO_REUSEPORT). RAM darboğaz değil (<1GB); CPU = aktif tx-rate ile ölçeklenir, tenant sayısıyla değil.
03.1 · Alt Yapı

Temel Hiyerarşi

C#/OOP mentalitesi: base/derived entity, value object, aggregate, interface. Tablo sayısı hedef değil; temiz entity modeli hedef.

Yönetici prensip — entity ≠ tablo
{{ p }}
Sınıf hiyerarşisi · UML

Capability interface'ler

God-base değil, ihtiyaç kadar opt-in. Ölü kolon yok.

«{{ c.t }}»
{{ c.d }}
03.2 · Alt Yapı

Value Object'ler

Immutable, self-validating, structural equality, kimliksiz. Kural: sorgulanan çok-alanlı VO → düz sibling kolonlar; bütün-okunan VO → typed jsonb. Mapper kurar; entity sadece doğrulanmış nesneyi görür.

{{ v.t }}
{{ v.d }}
04.2 · Veri Modeli

Polimorfik Entity'ler — STI

Drizzle native STI yok → tek tablo + kind + explicit per-kind check() + Mapper.fromRow + per-kind zod parse. Union domain-only.

Product — ABSTRACT · STI · tek tablo + kind + per-kind CHECK

Theme / BundleItem ayrı child tablo — kendi kimlik/ilişkileri var (STI değil, aggregate-child).

Grant (=Subscription) — AGGREGATE ROOT
status FSM
granting set = { active, trialing, grace_period }
kind: recurring | perpetual | trial · period: BillingPeriod · pricePaid: Money(snapshot) · links: GrantLinks. Children (kendi tabloları, SADECE kök üzerinden yazılır): GrantStatusLog (append-only durum geçişleri) + GrantEvent (renewal/dunning/analytics). Bundle alımı ⇒ 1 parent grant + üye başına 1 child grant; her biri bağımsız iptal-edilebilir, bağlı.
Entitlement invariant
entitlement = status ∈ granting ve start ≤ now ≤ (end|∞) olan TÜM Grant'ların Product.grantedFeatures() birleşimi (bundle açılır, deps closure katlanır).
Invoice «AGG ROOT»
lines: LineItem[] (ayrı invoice_line satırları); totaller issue'da snapshot'lanır.
Payment «LEDGER, append-only»
kind: charge|refund|chargeback, amount: Money(signed), parentPaymentId. Refund/chargeback = yeni satır; invoice.paid = sum(amount) projeksiyonu.
04.3 · Veri Modeli

Aggregate Sınırları

Kök üzerinden tek tx'te yüklenir/yazılır (R6). Cross-aggregate tutarlılık outbox + saga ile EVENTUAL — distributed tx YOK.

{{ a.root }}«root»
{{ a.children }}
Cross-aggregate tutarlılık (payment→invoice→grant, provisioning) outbox + saga ile EVENTUAL — bundle child grant'ları KENDİ kökleri, id ile bağlı; Payment bağımsız ledger, Invoice/Grant'a id ile referans.
03.3 · Alt Yapı

IModule Kontratı

SKU (satış) + NestJS modülü (kod) + Drizzle şema dilimi (veri) + manifest (deps/UI) + featureKey (entitlement). Pluggable = deploy-zamanı statik kod + per-tenant entitlement aktivasyonu; runtime hot-plug DEĞİL.

interface IModule {
manifest: ModuleManifest; // kod = davranış/deps/şema kaynağı
schema?: ModuleSchemaSlice; // pgTable\u2019lar (requiresSchema=false ise yok)
migrations: ModuleMigration[]; // additive-first; ON-ACTIVATION
repositoryPorts: RepositoryPortBinding[]; // port + per-dialect adapter (R8)
hostBinding: IModuleHostBinding; // in-process | out-of-process
lifecycle: IModuleLifecycle; // onActivate/Migrate/Deactivate — worker\u2019da
frontend: FrontendFeaturePackage; // nav + lazy route + slot (gated)
products: ModuleProductRegistration[]; // SKU\u2019lar → Product kataloğu
}
Aktivasyon TÜRETİLİR (elle değil)
Modül AKTİF iff EntitlementSet ∋ featureKey AND deps karşılandı AND (requiresSchema ise) migration'lar O tenant DB'sine uygulandı. Deps eksik → BLOCKED; satış katmanı auto-resolve eder.
03.4 · Alt Yapı

Dış Proje Entegrasyonu

"Any big project can come in" → EVET (clean + generic). Tek kontrat (IModule), 5 hizalı görünüm. Dış projenin veri nesneleri sadece extends TenantEntity → izolasyon / RLS / soft-delete / audit / optlock bedava — modülde sıfır izolasyon kodu.

Backend plug-in
Tablolar extends TenantEntity (prefix lic_/prd_/stk_/ai_); repo'lar domain port + Pg adapter; controller @RequireFeature (global guard DEFAULT-DENY); mevcut şema → ilk additive migration. Untrusted 3rd-party → out-of-process (signed + SAST + allowlist).
Frontend plug-in
FrontendFeaturePackage { featureKey, navEntries, routes(lazy), components(slot) }. Theme shell tenant'ın EntitlementSet'ini çeker, entitled featureKey başına paketi dinamik kaydeder. Gerçek authz backend @RequireFeature.

4 örnek — hepsi aynı 5 görünüm, branch yok

Schema-less + sandboxed + metered bir modül de aynı şekilde absorbe edilir.

({{ e.l }}){{ e.t }}
{{ e.key }}
{{ e.d }}
Dürüst caveat'lar
"pluggable" = deploy-zamanı kod + per-tenant aktivasyon (runtime hot-plug değil). Dış projeyi içeri taşımak gerçek refactor (repo→port, entity→TenantEntity, şema→migration, bespoke-auth→drop) — kontrat bunu mekanik/uniform yapar, sıfır-efor değil. MySQL "mimari hazır, bedava değil".
04.4 · Veri Modeli

ER Diyagramı — Control DB (26 tablo)

~30 domain entity sınıfı + ~15 value object = ~45 veri nesnesi. entity ≠ tablo: Product ailesi 5 sınıf → 1 tablo; VO'lar gömülü. Tenant-DB tabloları (company + modül lic_/prd_/stk_) ayrı; cross-DB FK yok.

{{ g.name }} {{ g.count }}
{{ t.t }}
{{ t.fields }}
İlişkisel diyagram · crow's-foot ERD
Tenancy & Altyapı
Commerce & Modüller
Polimorfik: refresh_token.subject_id · mfa_factor.subject_id · audit_log.actor_id (realm + subject_id; ER'de tek çizgiyle gösterilmez).
04.5 · Veri Modeli

UML Class Diyagramı

△ inheritance <|-- · ◆ composition *-- · ○ realize ..|>

IModule = servis kontratı (entity değil); modül entity'leri TenantEntity'den türer (ModuleRow).
03.5 · Alt Yapı

Kod & Klasör Yapısı

Hedef yapı (backend/ greenfield). Domain katmanı Nest'ten bağımsız POJO; her modül ports/ + adapters/pg/ ile izole.

+
{{ c.c }} {{ c.m }}

Repository spine + boot

{{ r.t }}
{{ r.d }}
03.6 · Alt Yapı

Out-of-Process Modül ABI

Karar: out-of-process out-of-process bugün desteklenmiyor; ancak genişleme noktası baştan hazır — type-level + tek forRoot dalı. İleride eklemek = sadece bir transport adapter yazmak.

type IModuleHostBinding =
| { kind:'in-process';     module: DynamicModule } // BUGÜN mount edilen
| { kind:'out-of-process'; transport: RpcTransportDescriptor; proxyModule } // tip var, bugün throw
RpcTransport = queue(brokerRef) | grpc(endpoint,tls) | http+mtls(caRef)
{{ a.t }}
{{ a.d }}
Tek gelecek-delta noktası
ModulesModule.forRoot: assert(manifest.isolation === hostBinding.kind) · in-process → imports.push(module) · out-of-process → bugün throw NotImplemented. Başka hiçbir şey değişmez.
03.7 · Alt Yapı

Frontend Dinamik Kayıt

In-process LRU memory derdi YOK. Build-time statik registry + Redis ui-manifest.

{{ f.t }} {{ f.d }}
GET /storefront/ui-manifest // signed-context auth · Redis ui:manifest:{tid}:{epoch}
{ tenant_id, epoch, features[]/*entitled+active*/, theme, customization?, status }
03.8 · Alt Yapı

Eng-Review Revizyonları

Mimari korunarak düzeltilen P0/P1 noktalar — doğruluk, güvenlik ve mapping.

{{ e.sev }} {{ e.t }}
{{ e.d }}
04.1 · Veri Modeli

Genel Bakış

C#/OOP mentalitesi: base/derived entity, value object, aggregate, interface. Tablo sayısı hedef değil; temiz entity modeli hedef.

{{ c.n }}
{{ c.t }}
{{ c.d }}
~45 VERİ NESNESİ TOPLAM
+
Yönetici prensip — entity ≠ tablo
{{ p }}
04.6 · Veri Modeli

Karar Bekleyen Noktalar

§8'in açtığı sorular; çoğu §9 fork kararlarında çözüldü.

{{ d.t }}
{{ d.st }}
{{ d.d }}
04 · DB-per-tenant Yük Testi

Topoloji ucuz, migration hızlı.

Tek Postgres 16 düğümü (max_connections=200, shared_buffers=256MB) + PgBouncer (transaction pool). Lokal ortam, tek makine. Sayılar eğilim/şekil içindir; mutlak rakamlar prod donanımında tekrar ölçülmeli.

/01

Kapasite

{{ c.state }}
{{ c.db }}
tenant DB
{{ c.ram }}
RAM
{{ c.cpu }}
CPU
{{ c.tput }}
throughput
Disk tenant başına ~7.6 MB sabit (boş şema + bir modül): 3000 ≈ 23 GB, 10k ≈ ~76 GB. RAM tenant sayısıyla büyümüyor, boştaki DB neredeyse maliyetsiz. CPU eşzamanlı işlem hızıyla artıyor, tenant sayısıyla değil.
/02

Sınırlar

{{ l.n }} {{ l.d }}
/03

Provisioning & Migration

İş
Hız
3000
10k
{{ p.job }}
{{ p.rate }}
{{ p.k3 }}
{{ p.k10 }}
Yeni DB açma template kopyasıyla serileşiyor (Postgres template'i kilitliyor); paralellik hızı artırmadı. Ani kayıt dalgası için kuyruk veya ön-hazırlık.
Fan-out hızlı. Ulaşılamayan DB karantinaya alınıp tekrar deneniyor, diğerlerini durdurmuyor. Expand/contract → kesinti yok. Yedek: pgBackRest WAL + paralel per-tenant pg_dump (139 ms/DB); seri pg_dumpall kullanılmaz.
Sonuç
Kaynak tarafında db-per-tenant ucuz ve migration hızlı. İki gerçek darboğaz: eşzamanlı bağlantı sayısı (düğüm başına max_connections) ve PgBouncer'ın tek çekirdekte doyması.
Her ikisi de bilinen yöntemlerle çözülüyor (shard, çok pooler). Karar: db-per-tenant.
05 · Implementation Roadmap

Spine-first, sonra tek dikey dilim.

Önce çekirdek omurga, sonra bir tenant'ı gerçekten canlıya çıkaran uçtan-uca ince dilim; sonra ilk gerçek modül; sonra sertleştirme. Güvenlik must-close'ları (§20) ait oldukları fazda yapılır, ertelenmez.

{{ f.n }}
{{ f.t }}
{{ p.n }}
{{ p.t }}
{{ p.hedef }}
Çıktılar
{{ o }}
Bağımlılık
{{ p.dep }}
Bitti
{{ p.done }}
/06

Ertelenmiş — genişleme noktası hazır

{{ d.t }}
{{ d.d }}