Monaf Horany
All work

001

2026

Megastore

A trilingual, multi-currency commerce platform: five applications behind one set of shared contracts, plus a Flutter client.

apps in the monorepo
5
shared packages
6
locales, RTL-first
3
currencies
5

Architecture

Megastore platform architectureBrowsers and the Flutter app reach the Next.js storefront and admin dashboard, which call a single NestJS API. The API is the only service that touches PostgreSQL. It also reads and writes Redis for caching and rate limiting, enqueues jobs onto BullMQ for a separate worker process, stores processed media in MinIO, and calls an internal Python service for background removal. The worker handles image and video transcoding, scheduled statistics rollups and notifications.CLIENTSAPPSSERVICESDATAenqueueBrowserwebFlutter appiOS · AndroidStorefrontNext.js 16DashboardNext.js 16NestJS APIsole DB ownerWorkerBullMQPostgreSQLPrisma 7Rediscache · queue · ratesMinIOprocessed mediamedia-pyFastAPI · rembg
Browsers and the Flutter app reach the Next.js storefront and admin dashboard, which call a single NestJS API. The API is the only service that touches PostgreSQL. It also reads and writes Redis for caching and rate limiting, enqueues jobs onto BullMQ for a separate worker process, stores processed media in MinIO, and calls an internal Python service for background removal. The worker handles image and video transcoding, scheduled statistics rollups and notifications.
  • Client
  • Service
  • Data store
  • External

The problem

Megastore is the official agent for TELETAB and OUKITEL in Syria. It needed a real commerce platform — not a themed template — serving customers in English, Arabic and Turkish, pricing in five currencies, and feeding a Flutter app from the same API as the web storefront.

A general-purpose store across electronics, clothing, cosmetics and health care, sold to three language markets in five currencies. Arabic is right-to-left and cannot be an afterthought bolted on at the CSS layer. Products vary by attribute, so the thing a customer actually buys is a variant, not a product. And the same catalogue has to serve a web storefront, an admin dashboard and a mobile app without three divergent definitions of what a product is.

The approach

I wrote the architecture down before writing any code — a specification with numbered decisions, each carrying its rationale and the alternatives I rejected. Then I built to it: a Turborepo monorepo with a NestJS API as the sole owner of the database, two Next.js applications, a BullMQ worker, and a small Python service for background removal. Shared Zod schemas sit in one package that every application imports, so the API validator and the dashboard's form resolver are literally the same object.

Decisions that mattered

  1. 01

    One source of truth for types

    Every shared schema is a Zod object in `packages/contracts`, consumed by NestJS through `nestjs-zod` and by React Hook Form through `zodResolver`. Five applications, one definition. A field cannot drift between the API that validates it and the form that submits it, because there is only one field.

  2. 02

    Two independent auth stacks

    Customers and admin staff share no tables, no tokens, no guards and no keypairs. Each has its own ES256 signing key and its own JWKS endpoint. A stolen customer token is not merely under-privileged against an admin route — it fails signature verification. The privilege boundary is cryptographic rather than conditional.

  3. 03

    Passwords never travel in plaintext

    The browser wraps the password in an AES-256-GCM envelope, encrypts the symmetric key with an RSA-OAEP-256 public key fetched from the API, and sends the envelope. TLS is still mandatory — this is defence in depth. What it buys is that a password can never surface in a proxy log, an APM trace or a captured request dump. Keys rotate every 24 hours in Redis, with the previous key honoured for a further 24.

  4. 04

    Process before you store

    Nothing unprocessed reaches object storage. An upload is sniffed by magic bytes to catch a spoofed content type, cropped, auto-rotated from EXIF, stripped of metadata, downscaled and re-encoded to WebP — all inside the request. One upload produces exactly one finished object. There is no staging bucket and no orphan cleanup job, because there are never any orphans.

  5. 05

    The variant is the product

    A `Product` is a thin grouping container; the `ProductVariant` carries price, currency, stock, slugs, media and category. Listings, facets, search, cart and orders all operate on variants. A simple product is just a product with one variant — which means there is no `isSimple` branch anywhere in the codebase. One code path, everywhere.

  6. 06

    Postgres first, Meilisearch later

    Generated tsvector columns with GIN indexes and `pg_trgm` for fuzzy matching cover launch-scale search and faceting with zero additional infrastructure. Meilisearch is specified as a drop-in enhancement that needs no schema change. Adding a container you do not yet need is a cost, not a capability.

Hard problems

  1. 01

    Filtering price across five base currencies

    Each variant is priced in its own base currency, but a customer filtering by price is thinking in one display currency. Converting in the application would mean loading every variant before filtering — unworkable. The filter is a SQL CTE that joins the exchange-rate table and compares `price × rate(variant.currency → filterCurrency)` against the bounds inside the query, so the database still does the filtering and the pagination stays correct. The same conversion drives price sorting.

  2. 02

    Making an encrypted password un-replayable

    Encrypting a password is not enough on its own — an intercepted envelope could simply be sent again. The plaintext payload carries a timestamp and a 128-bit nonce. The server rejects anything outside a 120-second window, then claims the nonce with a Redis `SETNX` on a five-minute TTL. A replayed envelope fails on the second attempt regardless of how it was obtained.

  3. 03

    Product video without a video platform

    Product videos needed adaptive playback, but self-hosted video platforms are built for live streaming and are heavy. Instead the worker probes the upload with ffprobe, builds only the rungs the source can support — 1080p, 720p, 480p — with keyframe-aligned GOPs so players can switch cleanly between them, and writes an HLS master playlist. Until the ladder finishes the original file plays progressively, so nothing is ever blocked on transcoding.

  4. 04

    Arabic as a first-class locale

    Right-to-left is a layout problem, not a translation problem. Layout uses logical properties throughout rather than left/right, so direction inverts without a parallel stylesheet. Localised database fields carry all three languages as JSON, and Arabic gets its own URL slug — Turkish deliberately reuses the English one, which is a routing convention that saves a column and a class of bugs.

My role

Sole engineer. Architecture, database design, API, both front ends, the worker, the media pipeline, the Flutter client, and the Docker infrastructure it all runs on.

Status

In active development. The v1 platform is a 424-file application with the full admin surface — catalogue, orders, refunds, media library, analytics reporting and a drag-and-drop homepage builder. The v2 workspace rebuilds it against the monorepo architecture described here. The iOS build is in App Store review.

Stack

Backend
NestJS 11Prisma 7PostgreSQL 16RedisBullMQZod
Frontend
Next.js 16React 19next-intlTailwind v4shadcn/ui
Media
SharpFFmpeg / HLSMinIOFastAPIrembg
Mobile
FlutterBloc / Cubitgo_routerget_itdio
Infrastructure
DockerTurborepopnpmInfisicalTraefikCloudflareUmami