all projects

case study 01

Lynor

Production

An e-commerce platform I designed, built and run in production, where orders stay correct under concurrency, retries and failure.

Lynor screenshot 1
Lynor screenshot 2
Lynor screenshot 3
Lynor screenshot 4
01 / 04

Lynor is an e-commerce platform for the Bangladesh market, live at lynorco.com since October 2026: a customer storefront, a staff dashboard and one API, in a pnpm and Turborepo monorepo. The market shapes the design. Payment is cash on delivery. Customers sign in with a phone number and an SMS code, not a password. Addresses follow the Division › District › Thana hierarchy, which the schema models directly, and shipping fees are priced per zone from those areas.

The hard part is not the storefront. It is keeping orders and stock correct when two shoppers reach for the last unit, when a request is retried, when two staff members act on the same order, and when an SMS provider is down.

  1. 01
    One NestJS API behind two Next.js apps, instead of full-stack Next.js. The storefront and the staff dashboard share one API, so the order rules live in one place, and neither frontend touches the database or ships Prisma in its bundle. Caddy serves the API on each site's own origin, so there is no CORS and the session cookies are first-party.
  2. 02
    A conditional stock update instead of locking and re-reading. Each order runs UPDATE … SET stock = stock - q WHERE id = $id AND stock >= q for every variant, sorted by variant id. If no row matches, the order aborts with what is available. This is correct under Postgres's default isolation, and the id ordering prevents deadlocks between orders that share products. No network calls happen inside the transaction.
  3. 03
    A transactional outbox instead of BullMQ. SMS and email jobs are rows written in the same transaction as the order, so a job can't be lost or sent for an order that rolled back. A poller claims them with FOR UPDATE SKIP LOCKED and retries with backoff (30 s doubling, up to six attempts). A job stuck mid-run is picked up again. Delivery is at-least-once, so handlers are idempotent. I'd switch to a queue at high volume, with several workers, or if I needed per-vendor rate limits.
  4. 04
    Opaque database sessions instead of JWTs. A random token in an httpOnly cookie, stored hashed. Blocking a customer, changing a staff role or resetting a password takes effect on the next request. The cost is one database read per authenticated request.
  5. 05
    Checkout that keeps working when SMS fails. If the SMS provider is down, the order is still accepted, but marked unverified: it can't be confirmed until staff call the customer, and it exposes nothing about the customer's account.
  1. 01
    Integration tests against real Postgres and Redis, including:
    • 10 concurrent orders for 3 units: exactly 3 succeed, stock ends at 0, no extra ledger rows.
    • Orders that list the same variants in opposite order don't deadlock.
    • Concurrent submits with the same idempotency key create one order.
    • Two concurrent status changes on one order: exactly one wins, and stock is restocked at most once.
  2. 02
    650+ API tests run before every merge and release, including a guard sweep that fails if any admin route answers without a staff session or declares no permission.
  3. 03
    Every order status change is a conditional update (WHERE status = from), so a stale click gets a 409 instead of overwriting.
  4. 04
    Cancellations and returns restock by reversing the order's own inventory movements, not by recomputing from the current bundle definition, which may have changed since.
  5. 05
    Operations: images built in CI and only pulled on the server; tagged releases deployed and rolled back with one command; a memory limit on every container; the database backed up hourly, encrypted, to off-site storage.
  1. 01
    An incident I caused. On launch evening I added my demo projects to the same server. One demo's API container was named api, the same name the reverse proxy used for Lynor's API, so for about a minute part of the store's API traffic went to the demo. Four requests failed, all of them my own checks. I fixed it by routing to reserved lynor-* names, and I added a test that fails if another service takes one of those names.
  2. 02
    Backups run hourly, but I haven't done a restore drill yet. That's the next operations task.
  3. 03
    One server. There's a rollback command and restore docs, but no failover.
  4. 04
    Each deploy causes about 12 seconds of errors on the API while its container restarts. Zero-downtime deploys are on the list.
  5. 05
    No courier integration or online payment yet. Both are planned.
  • NestJS
  • PostgreSQL 17
  • Prisma
  • Redis 7 (sign-in codes and rate limits)
  • Next.js (storefront and dashboard)
  • Turborepo
  • Docker Compose
  • Caddy
  • Cloudflare
  • Sentry

Built on Invariant, my design languageSet in Sora, Rethink Sans and Geist Mono