Monaf Horany
Tüm çalışmalar

001

2026

Megastore

Üç dilli, çok para birimli bir e-ticaret platformu: paylaşılan tek bir sözleşme katmanının arkasında beş uygulama ve ayrıca bir Flutter istemcisi.

monorepo içindeki uygulama
5
paylaşılan paket
6
dil, RTL öncelikli
3
para birimi
5

Mimari

Megastore platform mimarisiTarayıcılar ve Flutter uygulaması, Next.js mağazasına ve yönetim paneline ulaşır; bu ikisi tek bir NestJS API'sini çağırır. PostgreSQL'e dokunan tek servis API'dir. API ayrıca önbellekleme ve hız sınırlama için Redis'i okuyup yazar, ayrı bir işçi süreci için BullMQ'ya iş bırakır, işlenmiş medyayı MinIO'da saklar ve arka plan silme için dahili bir Python servisini çağırır. İşçi süreci; görüntü ve video dönüştürmeyi, zamanlanmış istatistik toplamayı ve bildirimleri üstlenir.CLIENTSAPPSSERVICESDATAenqueueBrowserwebFlutter appiOS · AndroidStorefrontNext.js 16DashboardNext.js 16NestJS APIsole DB ownerWorkerBullMQPostgreSQLPrisma 7Rediscache · queue · ratesMinIOprocessed mediamedia-pyFastAPI · rembg
Tarayıcılar ve Flutter uygulaması, Next.js mağazasına ve yönetim paneline ulaşır; bu ikisi tek bir NestJS API'sini çağırır. PostgreSQL'e dokunan tek servis API'dir. API ayrıca önbellekleme ve hız sınırlama için Redis'i okuyup yazar, ayrı bir işçi süreci için BullMQ'ya iş bırakır, işlenmiş medyayı MinIO'da saklar ve arka plan silme için dahili bir Python servisini çağırır. İşçi süreci; görüntü ve video dönüştürmeyi, zamanlanmış istatistik toplamayı ve bildirimleri üstlenir.
  • İstemci
  • Servis
  • Veri deposu
  • Harici

Problem

Megastore, TELETAB ve OUKITEL'in Suriye'deki resmi temsilcisi. Hazır bir tema değil, gerçek bir ticaret platformu gerekiyordu: müşterilere İngilizce, Arapça ve Türkçe hizmet veren, beş para biriminde fiyatlayan ve Flutter uygulamasını web mağazasıyla aynı API'den besleyen bir sistem.

Elektronikten giyime, kozmetikten sağlık ürünlerine uzanan genel bir mağaza; üç dil pazarına, beş para biriminde satış yapıyor. Arapça sağdan sola yazılır ve bu, CSS katmanında sonradan eklenecek bir ayrıntı değildir. Ürünler özniteliklere göre değiştiği için müşterinin gerçekte satın aldığı şey ürün değil, varyanttır. Üstelik aynı katalog; bir web mağazasına, bir yönetim paneline ve bir mobil uygulamaya, «ürün» kavramının üç farklı tanımı ortaya çıkmadan hizmet vermek zorunda.

Yaklaşım

Tek satır kod yazmadan önce mimariyi yazıya döktüm — her biri gerekçesini ve elediğim alternatifleri taşıyan, numaralandırılmış kararlardan oluşan bir teknik şartname. Sonra ona göre inşa ettim: Turborepo tabanlı bir monorepo, veritabanının tek sahibi olan bir NestJS API'si, iki Next.js uygulaması, bir BullMQ işçisi ve arka plan silme için küçük bir Python servisi. Paylaşılan Zod şemaları tek bir pakette duruyor ve her uygulama oradan içe aktarıyor; yani API doğrulayıcısı ile panelin form çözümleyicisi kelimenin tam anlamıyla aynı nesne.

Fark yaratan kararlar

  1. 01

    Tipler için tek doğruluk kaynağı

    Paylaşılan her şema, `packages/contracts` içinde bir Zod nesnesi; NestJS bunu `nestjs-zod` üzerinden, React Hook Form ise `zodResolver` üzerinden tüketiyor. Beş uygulama, tek tanım. Bir alanın, kendisini doğrulayan API ile kendisini gönderen form arasında ayrışması mümkün değil — çünkü ortada tek bir alan var.

  2. 02

    Birbirinden bağımsız iki kimlik doğrulama yığını

    Müşteriler ve yönetici personel hiçbir tabloyu, jetonu, guard'ı veya anahtar çiftini paylaşmıyor. Her birinin kendi ES256 imzalama anahtarı ve kendi JWKS uç noktası var. Çalınmış bir müşteri jetonu, bir yönetici rotasında yalnızca yetkisiz kalmıyor — imza doğrulamasında düpedüz başarısız oluyor. Yetki sınırı koşullu değil, kriptografik.

  3. 03

    Parolalar hiçbir zaman düz metin olarak gitmez

    Tarayıcı parolayı AES-256-GCM zarfına sarıyor, simetrik anahtarı API'den aldığı RSA-OAEP-256 açık anahtarıyla şifreliyor ve zarfı gönderiyor. TLS yine zorunlu — bu, katmanlı savunma. Kazancı şu: parola artık bir proxy log'unda, bir APM izinde veya yakalanmış bir istek dökümünde asla görünemez. Anahtarlar Redis'te 24 saatte bir dönüyor, bir önceki anahtar 24 saat daha geçerli kalıyor.

  4. 04

    Depolamadan önce işle

    Nesne depolamasına işlenmemiş hiçbir şey ulaşmıyor. Yüklenen dosya, sahte içerik tipini yakalamak için sihirli baytlarından doğrulanıyor; kırpılıyor, EXIF'e göre otomatik döndürülüyor, meta verisi temizleniyor, küçültülüyor ve WebP'ye yeniden kodlanıyor — hepsi isteğin içinde. Bir yükleme, tam olarak bir bitmiş nesne üretiyor. Ne bir ara kova var ne de artık dosya temizleme işi; çünkü hiçbir zaman artık dosya oluşmuyor.

  5. 05

    Varyant ürünün kendisidir

    `Product` ince bir gruplama kabı; fiyatı, para birimini, stoğu, slug'ları, medyayı ve kategoriyi `ProductVariant` taşıyor. Listeleme, filtreleme, arama, sepet ve siparişlerin tamamı varyantlar üzerinde çalışıyor. Basit ürün, tek varyantlı bir üründen ibaret — yani kod tabanının hiçbir yerinde `isSimple` dalı yok. Her yerde tek bir yürütme yolu.

  6. 06

    Önce Postgres, sonra Meilisearch

    GIN indeksli üretilmiş tsvector sütunları ve bulanık eşleştirme için `pg_trgm`, lansman ölçeğindeki arama ve filtrelemeyi sıfır ek altyapıyla karşılıyor. Meilisearch, şema değişikliği gerektirmeyen bir iyileştirme olarak şartnamede duruyor. Henüz ihtiyaç duymadığınız bir konteyner eklemek yetenek değil, maliyettir.

Zor problemler

  1. 01

    Beş farklı temel para biriminde fiyat filtresi

    Her varyant kendi temel para biriminde fiyatlanıyor, ama fiyata göre filtreleyen müşteri tek bir görüntüleme para birimiyle düşünüyor. Dönüşümü uygulama katmanında yapmak, filtrelemeden önce tüm varyantları belleğe almak demekti — uygulanabilir değil. Bunun yerine filtre, kur tablosunu birleştiren ve `price × rate` çarpımını sınırlarla sorgunun içinde karşılaştıran bir SQL CTE'si. Böylece filtrelemeyi veritabanı yapmaya devam ediyor ve sayfalama doğru kalıyor. Aynı dönüşüm fiyata göre sıralamayı da besliyor.

  2. 02

    Şifreli parolayı tekrar oynatılamaz kılmak

    Parolayı şifrelemek tek başına yetmez — araya girilen bir zarf yeniden gönderilebilir. Bu yüzden şifreli içerik bir zaman damgası ve 128 bitlik bir nonce taşıyor. Sunucu 120 saniyelik pencerenin dışındaki her şeyi reddediyor, ardından nonce'u Redis'te beş dakikalık `SETNX` ile sahipleniyor. Tekrar gönderilen bir zarf, nasıl ele geçirilmiş olursa olsun ikinci denemede başarısız oluyor.

  3. 03

    Video platformu olmadan ürün videosu

    Ürün videoları uyarlanabilir oynatma gerektiriyordu, ama kendi sunucunuzda barındırılan video platformları canlı yayın için tasarlanmış ve ağır. Bunun yerine işçi süreci dosyayı ffprobe ile inceliyor, yalnızca kaynağın destekleyebildiği basamakları üretiyor — 1080p, 720p, 480p — oynatıcı aralarında temiz geçiş yapabilsin diye anahtar kareleri hizalıyor ve bir HLS master listesi yazıyor. Basamaklar tamamlanana kadar orijinal dosya aşamalı olarak oynuyor; yani hiçbir şey dönüştürmeyi beklemiyor.

  4. 04

    Birinci sınıf bir dil olarak Arapça

    Sağdan sola yazım bir çeviri değil, bir yerleşim problemi. Yerleşim baştan sona sağ/sol yerine mantıksal özellikler kullanıyor, böylece yön paralel bir stil dosyası olmadan tersine dönüyor. Yerelleştirilmiş veritabanı alanları üç dili de JSON olarak taşıyor ve Arapça'nın kendi URL slug'ı var — Türkçe ise bilinçli olarak İngilizce slug'ı yeniden kullanıyor; bu, bir sütun ve bir hata sınıfı tasarruf ettiren bir yönlendirme kuralı.

Rolüm

Tek mühendis. Mimari, veritabanı tasarımı, API, her iki ön uç, işçi süreci, medya hattı, Flutter istemcisi ve hepsinin üzerinde çalıştığı Docker altyapısı.

Durum

Aktif geliştirmede. v1 platformu; katalog, siparişler, iadeler, medya kütüphanesi, analitik raporlama ve sürükle-bırak ana sayfa düzenleyicisiyle tam yönetim yüzeyini içeren 424 dosyalık bir uygulama. v2 çalışma alanı ise burada anlatılan monorepo mimarisine göre yeniden inşa ediyor. iOS sürümü App Store incelemesinde.

Teknolojiler

Arka uç
NestJS 11Prisma 7PostgreSQL 16RedisBullMQZod
Ön uç
Next.js 16React 19next-intlTailwind v4shadcn/ui
Medya
SharpFFmpeg / HLSMinIOFastAPIrembg
Mobil
FlutterBloc / Cubitgo_routerget_itdio
Altyapı
DockerTurborepopnpmInfisicalTraefikCloudflareUmami