Ahmed Abdelaziz
Back to projects
Custom Ecommerce Backend API for a Clothing Brand screenshot

Custom Ecommerce Backend API for a Clothing Brand

A production-grade TypeScript/Express REST API powering a full-stack clothing commerce platform — customer accounts, catalog variants, inventory ledger, carts, transactional checkout, orders with a status-transition state machine, reviews, coupons, analytics, financial PDF reports and an append-only audit trail with signed ImageKit uploads.

typescriptexpressnode.jspostgresqlprismazodsession-authenticationcsrfimagekitresendpinovitestpdfkit

August 2026

Overview

This project is the backend for a custom online clothing store and the data owner for a full-stack platform. A separate Next.js storefront and admin console — ecommerce-client — handles the UI; this API owns every business rule and every row in PostgreSQL.

The API is intentionally JSON-only. Browsers never talk to it directly in production: the storefront rewrites /api/v1/* to the API origin at the edge, so session cookies stay first-party and CORS disappears. The live platform runs at Live Demo (Vercel) → (Render + Neon PostgreSQL).

It is built to survive real commerce problems — two customers buying the last shirt at the same time, a price edit after someone already checked out, a stolen CSRF token, a coupon that should not be redeemable twice — not just to serve happy-path CRUD.

The Problem

A clothing store needs more than a product list. One product can have several purchasable combinations, such as a black medium shirt and a blue large shirt. Each combination carries its own SKU, barcode, price, discount, dimensions, images, and stock. The system must also keep an order's historical details stable after a product changes, prevent overselling under concurrent requests, and give administrators auditable control over catalog, inventory, and fulfillment — without scattering that logic across routes.

The Solution

The API separates each business area into a focused module and keeps HTTP handling, business rules, and database queries in different layers. Customers use prefixed public identifiers (usr_…, prd_…, ord_…) rather than internal integer IDs. Commerce-critical work is performed transactionally so checkout either completes as one consistent operation — stock reservation, coupon redemption, order snapshots, payment record, cart clear — or leaves no partial order behind.

The model is especially appropriate for apparel because variants explicitly support size and color, while also retaining SKU, barcode, pricing, dimensions, images, and inventory per variant. Orders snapshot every purchased value at checkout time so later catalog edits cannot rewrite history.

Key Features

Customer Accounts and Sessions

Customers can:

  • register (email + phone + password with complexity rules) and receive a 24-hour email-verification link
  • log in, view all active sessions per device, revoke any single session or all other sessions, log out
  • verify email, resend verification, request password reset via email link or 6-digit OTP (15 min TTL, attempt-limited), complete reset (revokes all sessions), change password, change email (verified via link), change phone (OTP), update profile, soft-delete own account
  • manage an address book (CRUD, default shipping/billing flags, ownership-scoped)

Authentication is opaque session-based, not JWT: a raw 32-byte token is hashed (SHA-256 + server pepper) and stored in PostgreSQL; the raw value is returned as an httpOnly, SameSite=Lax, Secure in production cookie. Every authenticated request validates hash, revocation, expiry, active account status, and a 14-day idle timeout. A double-submit CSRF token bound to the session (x-csrf-token header + __Host-csrf cookie) protects all writes — public writes are skipped by design, everything else fails closed in production. Brute-force lockout (10 failed logins / 15 min per email) and per-endpoint rate limiters cover credential and OTP paths.

Why it matters: Customers can use the store across devices while the server retains instant, row-level control of session validity, device visibility, and revocation.

Product Catalog and Variants

Public (no auth) can browse products (search by name/brand/description, brand filter, sort by name/created_at/updated_at, pagination), view product detail with ordered images and active variants with computed final prices, list categories, and list products per category. Customer visibility is strictly product not soft-deleted AND has ≥1 ACTIVE non-deleted variant.

Administrators can perform full CRUD on products (auto-slug with -2 suffix on collision, soft-delete cascading to variants), variants (SKU globally unique, nullable status, barcode/dimensions/cost), product images and variant images (hard delete, display_order auto max+1, exactly-one-primary invariant, ImageKit-signed direct uploads validated for host/folder/extension). Categories are reusable with is_active flag, soft-delete, and idempotent product assignment.

Why it matters: A customer selects an exact purchasable item rather than an ambiguous parent product, so stock and pricing remain tied to the right SKU, and the admin surface cannot create duplicate slugs, orphaned links, or galleries without a primary image.

Stock-Aware Cart

Authenticated customers maintain a single lazily-created server-side cart: add variants (merges quantity if already present), change quantity, remove a line, view the current cart with resolved product/image info and live server-side pricing, and clear it. All four mutations share the same pg_advisory_xact_lock(userId) checkout uses — concurrent add/update/remove against a simultaneous checkout serialize into an explicit 404 rather than a 500.

Why it matters: The cart represents real variants with live stock checks and is safe under race conditions before checkout.

Checkout and Orders

Checkout (POST /api/v1/orders) validates the cart and owned shipping address, checks variant ACTIVE status and available stock via a guarded conditional UPDATE … WHERE available >= qty, calculates subtotal/discount/shipping/tax with Decimal math (serialized as fixed strings), applies and increments coupon usage inside the same transaction, reserves/commits stock, creates immutable order_items snapshots (product name/slug/SKU, variant attributes including dimensions, unit price/discount/total), records a MockPaymentGateway payment (pluggable PaymentGateway interface), creates a shipments row that copies the selected address, asserts affected-row counts to abort on any mismatched precondition, and clears the cart. The entire flow runs inside one Prisma transaction guarded by a per-user advisory transaction lock.

Orders expose customer order history/detail with immutable snapshots and a strict status-transition state machine (PENDING → CONFIRMED → PROCESSING → SHIPPED → DELIVERED, with CANCELLED before fulfillment restoring coupon quota and releasing stock, and RETURNED → REFUNDED). Transitions lock the row with SELECT … FOR UPDATE and assert side effects.

Why it matters: A product edit made later does not rewrite what a customer bought, concurrent checkouts cannot silently oversell or double-charge, and every status change has an explicit, tested side effect.

Coupons and Shipping Rules

Coupons support fixed-amount and percentage discounts, minimum order values, maximum discount caps, validity windows (starts_at/expires_at), global usage_limit, and per-user usage_limit. coupon_usages records every redemption (unique on orders_id) and guarded decrements of usage_count restore quota on pre-fulfillment cancellation. Shipping uses a flat fee below a configured threshold and free shipping at or above it.

Why it matters: Promotion rules are applied consistently by the API inside the checkout transaction instead of being trusted to a client application, and concurrent redemptions cannot exceed limits.

Reviews and Moderation

Customers can create one live review per product (with images via the same signed ImageKit flow, host/folder/extension provenance check), update, and delete their own review, list their own reviews, and view product reviews; public product review listing is paginated. Reviews include ratings (1–5), optional title/text, attached images, and is_approved flag; purchase-verification is implemented behind REVIEWS_REQUIRE_PURCHASE (currently off). Administrators have moderation endpoints and aggregate rating data.

Why it matters: The store can collect customer feedback while retaining an administrative review boundary that prevents review-image URL planting.

Administration, Analytics, Reports, and Audit

Role-protected endpoints cover users, products, categories, inventory, orders, reviews, coupons, reports, and system health:

  • Roles: CUSTOMER → ADMIN → SUPER_ADMIN. authentication + authorization(ADMIN) guards admin routers; role changes, admin-account management (/admin/admins), audit log, analytics (overview/coupons/expenses P&L) and reports require SUPER_ADMIN. An admin:create CLI (npm run admin:create / superadmin:transfer) promotes an existing user to ADMIN/SUPER_ADMIN, rather than exposing privilege escalation through the public API. The last-admin protection prevents removing the final SUPER_ADMIN.
  • Inventory: Per-variant ledger (quantity_on_hand, quantity_reserved, reorder_level) with reserve/commit/release semantics for order fulfillment and manual admin adjustments.
  • Analytics: Dashboard stats plus P&L with manually recorded operating_expenses (rent/salaries/marketing/…) to compute true net profit alongside COGS.
  • Reports: Three financial PDFs — P&L Statement, Expenses, Revenue — by month|quarter|year|custom (≤366d, half-open UTC) with granularity auto|day|month and configurable currency (USD|EUR|GBP|EGP|SAR|AED). Vector tables + line/bar/pie charts via pure pdfkit (no native canvas/Chromium), streaming application/pdf with Cache-Control: no-store, Content-Disposition: attachment|inline and X-Report-Currency; ?format=json returns the same data for preview. Uses the same P&L aggregates as analytics (SUM(subtotal−discount), SUM(quantity×cost_price) COGS, operating_expenses by spent_at) with zero-filled series.
  • Audit: Append-only audit_logs table (never updated/deleted by the app) records actor, action, entity, method/path/status, request body (sensitive keys redacted), previous values/diffs, IP and user agent for every authenticated POST/PUT/PATCH/DELETE on /admin/* plus business events (order placed, coupon redeemed).
  • Uploads: GET /api/v1/uploads/imagekit-auth and GET /api/v1/admin/products/uploads/imagekit-auth issue short-lived HMAC parameters; product/variant binaries are uploaded directly from the browser to ImageKit, then the client registers the returned HTTPS URL via admin image endpoints.

Why it matters: Privileged actions are auditable, promotion is operator-only, large image files never flow through the API, and the ImageKit private key never leaves server configuration.

User Experience

The API enables these primary flows:

  1. A customer registers through the storefront and verifies the account via the email link served by the API (/verify-email static page).
  2. The customer logs in and receives an httpOnly session cookie, fetches a CSRF token, and browses products with search/filter/sort.
  3. The customer chooses a size/color variant (live availability + computed final price) and adds it to a cart.
  4. The customer selects a saved address and optionally applies a coupon during checkout; shipping rules settle automatically.
  5. The API creates order snapshots, payment record, shipment, stock changes, and coupon usage as one transaction and clears the cart.
  6. The customer lists order history, opens order detail (immutable invoice), and submits a review with images.
  7. An administrator manages catalog, stock, order status through the FSM, coupon lifecycle, review approval, user suspension, and views stats/analytics/audit log — or downloads a P&L/Expenses/Revenue PDF for a month, quarter, year or custom range with a chosen currency.

How It Works

All versioned API routes are mounted below /api/v1 (77 paths). The Express application also exposes /health (liveness) and /health/ready (readiness with 2s DB probe) outside the rate limiter, plus three small static pages for email verification, email-change verification, and password reset.

Request Flow

Checkout Flow

Checkout runs inside a service-owned Prisma transaction guarded by pg_advisory_xact_lock(userId). It verifies the cart is non-empty and all items are ACTIVE, verifies the owned address, conditionally reserves stock (available >= qty), calculates totals with Decimal, applies a coupon (uppercased) and conditionally increments usage_count, creates the order with order_number and immutable order_items snapshots, records a mock payment, commits stock, confirms the order, creates a shipments copy of the address, writes coupon_usages and audit_logs rows where applicable, and clears the cart. Any precondition mismatch or zero-affected-row guard throws and rolls the transaction back. The same advisory lock guards all four cart mutations so concurrent cart vs checkout requests serialize cleanly.

Architecture

The project uses a layered architecture organized by business module (16 domains). Dependencies move downward from HTTP routing toward persistence. In production the two-app topology keeps cookies first-party via a Next.js rewrite proxy.

  • Application and middleware: src/app/index.ts assembles Express, trust proxy (env TRUST_PROXY), helmet, CORS (single CORS_ORIGIN), JSON parsing, cookies, requestId, global rate limiter (skipped in test), CSRF (doubleCsrf bound to session), audit-log middleware on /admin/*, and the central errorHandler with Prisma P2002/P2025/22001 → 409/404/400 mapping.
  • Routes and controllers: Module routers declare method + path + zod schema + required roles; controllers translate HTTP input/output, never own DB queries or checkout rules.
  • Services: Services implement account, catalog, cart, inventory, review, order, coupon, analytics and reporting behavior. They coordinate repositories and own multi-write transactions and advisory locks.
  • Repositories: Repositories contain Prisma queries, ownership filters, pagination, joins, and selected raw SQL for derived inventory and reporting fields (including date_trunc P&L aggregates). They accept either the normal Prisma client or the transaction client.
  • Shared modules: Typed errors, validation helpers (shared passwordField, zod chains), pagination, constants (TRUST_PROXY, ENABLE_CSRF), logger, mailer, SMS stub, ImageKit adapter, and request types are reused across domains. Secrets are zod-validated at boot via src/config/env.ts and the process exits on missing/invalid config.
  • External adapters: Integrations sit behind small boundaries (Mailer, SmsProvider, PaymentGateway interface + MockPaymentGateway, ImageKit signed params) so email, payment, SMS, and image behavior can be replaced or tested independently. Uploaded image URLs are re-validated for host, folder allowlist, and file extension.

Technical Implementation

API Surface

The versioned API currently exposes 77 paths under /api/v1 (see openapi/openapi.yaml and hand-maintained contracts in docs/api/**):

AreaResponsibilities
AuthenticationRegister, login, logout, GET /auth/session + GET /auth/csrf-token, session list/revocation, verification (register), password reset via link + OTP
Users and addressesProfiles, account deletion, password/email/phone change flows, saved shipping addresses (default flags, ownership-scoped)
CatalogProducts, variants, categories, product and variant images (ordered, one primary)
InventoryPer-variant stock ledger with reserve/commit/release, manual admin adjustments and reservations
CartSingle lazily-created cart: variant line items, quantities, clear
OrdersCustomer checkout + order history/detail; admin order queue + FSM transitions (confirm/processing/ship/deliver/cancel/return/refund)
ReviewsCustomer create/edit/delete own review, product reviews, my reviews; admin moderation
CouponsAdmin coupon CRUD, usage inspection, coupon_usages history
ReportsP&L Statement, Expenses, Revenue PDFs by `month
AdministrationRole-protected management across domains; admin accounts (SUPER_ADMIN), stats, analytics (overview/coupons/expenses P&L), reports, audit log
UploadsSigned ImageKit auth for direct browser uploads (/uploads + /admin/products/uploads)

Successful responses use a { success: true, data } envelope; lists add { meta: { page, limit, total, totalPages, hasNext, hasPrev } } (offset-based, capped at 100). Errors use typed application errors and a consistent { success: false, message, errors? } shape. Unknown /api/* paths return the JSON envelope (never HTML).

Authentication and Authorization

Authentication is session-based rather than JWT-based. A successful login stores a SHA-256 hash (peppered with SESSION_SECRET) of the opaque session token in PostgreSQL and returns the raw token in the session cookie (httpOnly, SameSite=Lax, Secure in production). The authentication middleware hashes the cookie on later requests, checks revocation, expiry, 14-day idle timeout, and active account status, touches last_activity_at, and attaches req.userId/req.user/req.authSession.

The database defines CUSTOMER, ADMIN, and SUPER_ADMIN roles. authorization(ADMIN) middleware checks the authenticated role; services enforce ownership (carts, addresses, orders, reviews). The only privilege escalation path is the admin:create / superadmin:transfer CLIs, which promote an existing user — the public API cannot change roles beyond CUSTOMER self-registration.

Passwords are hashed with bcrypt (12 rounds, strong policy: /(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}/). Verification and password-reset tokens are stored as hashes with single-use lifecycle, expiry, and attempt counters. Email is sent through Resend (non-blocking; failures are logged without turning a successful account operation into a failed request); phone OTP delivery currently uses an SMS development stub that logs through pino.

Validation and Error Handling

Zod schemas validate request bodies, query strings, and route parameters at the route boundary before controllers run (one schema per endpoint, including body+query+params together). Business validation remains in services for rules such as ownership, purchasability, coupon applicability, available stock, and FSM legality.

A typed error taxonomy (BadRequest, Unauthorized, Forbidden, NotFound, Conflict, Gone, TooManyRequests, …) flows to one central handler; Prisma P2002 → 409, P2025 → 404, and Postgres 22001 string-too-long (via cause-chain depth ≤5) → 400 via prismaErrorMapper so database internals never leak. Unexpected failures return a generic 500 rather than implementation details. Sensitive keys are redacted from logs and audit records.

Database and Data Model

PostgreSQL is accessed through Prisma ORM 7 and the @prisma/adapter-pg driver adapter (with prisma.config.ts and @prisma/client output under src/generated/prisma). Money uses fixed-precision Decimal(10,2) (12,2 for operating_expenses) serialized as strings end-to-end and computed with decimal math (never floats); timestamps are timestamptz. The schema uses internal auto-incrementing integer PKs plus stable prefixed public IDs (usr_…, prd_…, var_…, ord_…, pimg_…, vimg_…, cat_…, adr_…, ses_…); internal IDs are never serialized into API responses. There are 24 tables including audit_logs and operating_expenses.

Important relationships are shown below. The complete schema is in prisma/schema.prisma (and documented in docs/DATABASE.md).

Order items copy product name, slug, SKU, variant attributes (color/size/dimensions), price, discount, and totals at checkout. Shipment rows also copy the selected address. These snapshots preserve historical order data when catalog or address records later change.

Commerce Consistency

  • Checkout uses a per-user pg_advisory_xact_lock transaction lock and conditional stock-reservation UPDATE … WHERE available >= qty — overselling is structurally impossible.
  • All four cart mutations (addCartItem, updateCartItemQuantity, removeCartItem, clearCart) route through the same withUserCartLock advisory lock so cart vs checkout races degrade to documented 404s, never 500s.
  • Inventory distinguishes quantity_on_hand and quantity_reserved and provides reserveStock/commitStock/releaseStock for order FSM transitions, plus manual admin reserve/release.
  • Coupon usage is checked and incremented inside the checkout transaction with a conditional guard and a coupon_usages row (unique on orders_id); pre-fulfillment cancellation restores quota (delete coupon_usages + guarded usage_count decrement), post-fulfillment refunds keep it consumed.
  • Order status follows an explicit allowedTransitions map executed under SELECT … FOR UPDATE, with side effects (shipment creation, stock commit/release, payment status, coupon quota) asserted by affected-row counts.
  • Repositories accept either the normal Prisma client or the transaction client, allowing service transactions to reuse the same query methods.
  • List endpoints commonly run count + page queries in parallel and use explicit sort tie-breakers for stable pagination; search LIKE patterns are escaped.
  • Inventory availability and stock status that are not directly expressible through typed Prisma filters are derived with schema-aware raw SQL queries.
  • DELETE on admin-owned resources uses soft deletes (deleted_at) for ten tables; customer cart line removal is hard delete by variant public ID; audit_logs is append-only.

Media and Integrations

  • ImageKit: An authenticated user or administrator requests short-lived HMAC upload parameters from GET /api/v1/uploads/imagekit-auth or GET /api/v1/admin/products/uploads/imagekit-auth (SDK v7.10.0 ImageKit({ privateKey }) — public key/endpoint served from env). The browser uploads directly to ImageKit, then registers the returned HTTPS URL through an admin image endpoint. Persisted URLs are re-validated for host, folder allowlist, and extension. The API stores URLs, not binary files.
  • Resend: Registration, email-change, and password-reset flows send transactional messages asynchronously via src/shared/mailer (templates in src/shared/mailer/templates). Failures are logged without turning a successful account operation into a failed request.
  • SMS: The current adapter (src/shared/sms) is a development stub that logs instead of sending.
  • Payments: PaymentGateway is an interface with a MockPaymentGateway implementation; the current checkout records mock payment references (pluggable Paymob integration is a tracked task).

Logging and Operations

Pino writes structured records to the terminal (pretty in development, structured JSON in production) and size-rotated NDJSON to logs/app.ndjson (silent in test so CI needs no log writes; route-level limiters intentionally stay active in tests while the global limiter is skipped). Request logs include method, URL (query redacted to pathname — single-use tokens in query strings must not be persisted), status, duration, x-request-id correlation, user/IP/user-agent, and stashed error context (res.locals.error). LOG_LEVEL filtering and secret redaction are globally applied; every /admin/* mutation also writes an audit_logs row.

Environment configuration is parsed with Zod at startup (src/config/env.ts) and the process hard-exits on missing/invalid vars. Required configuration includes DATABASE_URL, SESSION_SECRET (≥16 chars, invalidates all sessions on rotation), CORS_ORIGIN (must equal the public storefront origin — drives both CORS and every CORS_ORIGIN-based email link), TRUST_PROXY, RESEND_*, IMAGEKIT_*, PORT, NODE_ENV, and LOG_LEVEL/ENABLE_CSRF (boot fails when disabled in production). Secrets are not documented here.

Testing

Vitest 4 is the single test runner ( fileParallelism: false because DB-backed tests share one schema), with Supertest for HTTP-level API tests. The repository contains ~1,132 tests across 78 files (as of reports epic):

  • Unit tests for validators, token helpers, OTP behavior, slugs, sorting, stock rules, stock math, and other pure logic.
  • Integration tests against real PostgreSQL for repository and service behavior (including race suites: registration, product slug, category assign, cart vs checkout, last-admin protection).
  • API/e2e tests through the exported Express app for authentication, authorization (401/403), response contracts, CSRF 403s, validation, and resource endpoints.

External services are mocked or represented by development adapters. Test configuration uses a dedicated .env.test (real DB with a name containing test; the suite refuses to run against a non-test database) and disables parallel file execution because database-backed tests share a schema. CI runs typecheck (app + tests separately), build, migration deploy, boot smoke (/health probe), and the full suite.

Challenges and Solutions

Challenge: Preventing Overselling During Concurrent Checkout

Problem: A simple read-then-write stock check can allow two requests to purchase the same last item; cart mutations racing checkout can also surface 500s.

Solution: Checkout serializes per user via pg_advisory_xact_lock and uses guarded conditional UPDATEs for stock reservation; the four cart mutations are routed through the same lock, with zero-affected-row guards degrading races to documented 404s. Every race has a regression test: cart.race.integration.test.ts (checkout vs clear/update), cartConcurrency.api.test.ts (HTTP duplicate deletes), registerRace, productSlugRace, categoryAssignRace, last-admin protection.

Result: Stock reservation, coupon redemption, order creation, payment recording, and cart clearing share one rollback boundary and concurrent access is proven safe by tests.

Challenge: Preserving Historical Order Information

Problem: Product names, variant attributes, prices, coupons, or saved addresses can change after a purchase.

Solution: The service copies relevant product, pricing, and address values into order_items and shipments snapshots at checkout time.

Result: Order history, invoices, returns, and analytics remain readable and accurate independently of current catalog or address data.

Challenge: Keeping Coupon Limits Correct Under Concurrency

Problem: Checking coupon usage and incrementing it as separate unguarded operations could exceed global or per-user limits.

Solution: Usage is checked and incremented inside the checkout transaction, with conditional UPDATE guards on usage_count/usage_limit and a coupon_usages row; cancellation restores quota atomically, post-fulfillment refunds deliberately keep it consumed.

Result: Coupon rules are applied by the same atomic operation that creates the order, and race attempts are rejected with 409.

Challenge: Supporting Image Management Without an API Upload Proxy

Problem: The repository has no product frontend bundled and the API should not receive large binary image bodies.

Solution: The ImageKit adapter generates short-lived signed credentials; the client uploads directly and submits only the resulting URL, which the API re-validates for host, folder, and extension.

Result: The API remains JSON-oriented and the private ImageKit key never leaves server configuration.

Technology Stack

TechnologyPurpose
Node.js and Express 5HTTP server, routing, middleware, and REST API (77 paths)
TypeScript, strict mode (ESM)Static type checking and explicit contracts
PostgreSQL (Neon in production)Relational persistence, constraints, advisory locks, 2s readiness probe
Prisma ORM 7 with @prisma/adapter-pgTyped database access using the PostgreSQL driver adapter
ZodRuntime validation for environment configuration, shared passwordField, and every request's body/query/params
bcrypt and nanoidPassword hashing (12 rounds), opaque tokens, and prefixed public IDs
csrf-csrf (double-submit)CSRF protection bound to the session cookie
ImageKit (@imagekit/nodejs 7.10)CDN-backed product/variant/review image storage with signed direct uploads
ResendTransactional email delivery (verification, password reset, email change)
PinoStructured request/application logging with request-ID correlation and secret redaction
pdfkitStreaming application/pdf generation — vector tables + line/bar/pie charts, no native canvas/Chromium
Vitest 4 and SupertestUnit, integration (real PG), and HTTP API testing (78 files / 1,132 tests)
Helmet, CORS, cookies, rate limitingHTTP hardening and request controls (TRUST_PROXY-aware, per-endpoint limiters, global limiter skipped in test)

Project Structure

project-root/
├── src/
│   ├── app/                         Express assembly, middleware pipeline, static pages
│   ├── config/                      Environment validation (zod), Prisma client
│   ├── modules/
│   │   ├── auth/                    Register/login/session/CSRF/email-verify/password-reset
│   │   ├── users/                   Profiles, password/email/phone change, account deletion
│   │   ├── addresses/               Address book CRUD with default-flag invariants
│   │   ├── products/                Products/variants/images + ImageKit signed-upload adapter
│   │   ├── categories/              Categories + product assignment + is_active visibility
│   │   ├── inventory/               Per-variant stock ledger, reserve/commit/release
│   │   ├── cart/                    Lazily-created cart + advisory-locked mutations
│   │   ├── orders/                  Checkout, order history/detail, FSM transitions
│   │   ├── reviews/                 Customer reviews + images + flag-gated purchase verification
│   │   ├── coupons/                 Coupon lifecycle + usage history
│   │   ├── uploads/                 ImageKit auth endpoint
│   │   ├── admins/                  Admin account administration (SUPER_ADMIN)
│   │   ├── analytics/               Overview/coupon/P&L analytics with operating_expenses
│   │   ├── reports/                 P&L / Expenses / Revenue PDFs by period (pdfkit vector charts)
│   │   ├── audit/                   Append-only audit trail
│   │   └── stats/                   Dashboard stats
│   ├── middleware/                  requestId, rateLimiter, csrf, auth, auditLog, errorHandler, prismaErrorMapper
│   ├── shared/                      errors, logger (pino), mailer, sms stub, imagekit, pdf (pdfkit vector charts), constants, utils, types, validation
│   └── routes/                      Versioned router mounting under /api/v1 + unknown-/api 404 envelope
├── prisma/
│   └── schema.prisma                24 PostgreSQL models (incl. audit_logs, operating_expenses), enums, indexes
├── tests/
│   ├── unit/                        Pure logic and validator tests
│   ├── integration/                 Service/repository tests with real PostgreSQL + race suites
│   └── e2e/                         HTTP API tests through Supertest (incl. concurrency + CSRF flows)
├── scripts/
│   ├── create-admin.ts              Operator CLI for ADMIN promotion
│   ├── transfer-super-admin.ts      Operator CLI for SUPER_ADMIN transfer
│   └── cleanup-sessions.ts          Session cleanup cron
├── public/                          verify-email, verify-email-change, reset-password static pages
├── openapi/
│   └── openapi.yaml                 OpenAPI 3.1 spec (77 paths) — machine projection of docs/api/**
├── docs/                            Architecture, API contracts, database, operations,
│                                    testing, ADRs, and task ledger
└── tasks/                           87-item numbered task ledger (T-001…)

Deployment and Configuration

The live platform is Render (API) + Neon (PostgreSQL) + Vercel (storefront proxy) — the supported split-hosting topology keeps browser traffic same-origin via next.config.ts rewrites so SameSite=Lax session cookies work without code changes:

Browser ──► Vercel (storefront)
              │  same-origin /api/v1/* rewrites
              ▼  next.config.ts rewrites ──► Render (this API) ──► Neon PostgreSQL

API service (Render/Railway/Fly):

  • Build: npm ci && npm run build (postinstall runs prisma generate).
  • Start: npm run db:migrate:deploy && npm start — applies pending migrations, then boots dist/index.js.
  • Health checks: /health (liveness, always 200) and /health/ready (readiness, 2s DB probe → 200 or 503) live outside the rate limiter.
  • Env vars: from .env.example into the platform secret store. CORS_ORIGIN must equal the public storefront origin (drives both CORS and every CORS_ORIGIN-based email link); TRUST_PROXY=1 (one proxy hop); SESSION_SECRET rotation kills all sessions; ENABLE_CSRF defaults to true and production boot fails when disabled.

Local development commands: npm run dev (tsx watch src/index.ts), npm run build → npm start, npm run db:migrate/db:migrate:deploy/db:generate, npm test (full Vitest suite — never against a development or production DB; .env.test required), npm run typecheck, npm run admin:create, npm run sessions:cleanup.

CI validates on every pull request to main via GitHub Actions: install → generate Prisma → typecheck (app + tests) → build → provision PostgreSQL 16 → prisma db push → .env.test from repository secrets → full suite (T-035/T-040 concurrency suites included) → boot smoke probe. This validates the project in CI but is not a production deployment pipeline; deployment is via the Runbook in docs/DEPLOYMENT.md and docs/OPERATIONS.md (backups, restore drills, multi-instance rate-limit caveats, rotation procedure).

Environment variables are validated at startup — database credentials, session secrets, email credentials, and ImageKit keys must be supplied through the environment and are intentionally not included in this document. See docs/DEPLOYMENT.md §1 for the full inventory.

What This Project Demonstrates

Product and Business Perspective

  • Translating a clothing store's needs into reusable products, purchasable size/color variants, stock ledger, lazily-created carts, transactional orders with an explicit FSM, reviews with moderation, promotion rules with global/per-user limits, and P&L analytics alongside an immutable audit trail.
  • Designing customer and administrator flows around real commerce rules — stock races, idempotent history, review-image provenance, coupon quota restoration — rather than only CRUD screens.
  • Making shipping, payment (pluggable PaymentGateway), fulfillment states, and administrative promotion explicit, operator-owned, and audited.

Engineering Perspective

  • Uniform, module-per-feature layered architecture (routes → controller → service → repository → Prisma) with thin controllers, transaction-owning services, and repository-owned queries — across 16 domains, plain function composition, no classes/DI.
  • Session security with peppered hashed tokens, httpOnly cookies, double-submit CSRF, per-email lockout, per-endpoint rate limiters, TRUST_PROXY-aware IP resolution, bcrypt password hashing, and boot-time zod env validation that fails closed.
  • Transactional checkout and order FSM with pg_advisory_xact_lock + guarded conditional writes + SELECT … FOR UPDATE and affected-row assertions; every race has a named regression test (cart, registration, slug, assign, last-admin).
  • Typed PostgreSQL access, public/private ID separation, Decimal money, soft deletes, immutable snapshots, structured pino logging with request-ID correlation and secret redaction, health/readiness probes, streaming PDF generation with pure pdfkit, and external-service boundaries that keep provider keys off the wire.
  • Vitest + Supertest unit/integration/e2e testing (~1,132 tests, 78 files) against documented contracts, with third-party services isolated and the suite self-protecting against running on a non-test database.

Scope Notes

This repository is the backend of a full-stack platform, not a complete hosted storefront by itself. The current payment adapter is a mock (MockPaymentGateway; real Paymob integration is a tracked task), the SMS integration is a development stub, purchase-verification for reviews is flag-gated off (REVIEWS_REQUIRE_PURCHASE = false), there is no production multi-instance Redis rate-limit store yet (in-memory limits multiply by instance count — documented mitigation via single instance or Redis store), and the frontend application lives in a separate repository (ecommerce-client). Those boundaries are intentional and tracked as tasks (e.g. T-081–T-087, T-009, T-011/T-054) rather than undocumented gaps — see docs/api/** and content/design-docs/** for the per-module contract and PROJECT_PROGRESS.md for provenance.