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 requireSUPER_ADMIN. Anadmin:createCLI (npm run admin:create/superadmin:transfer) promotes an existing user toADMIN/SUPER_ADMIN, rather than exposing privilege escalation through the public API. The last-admin protection prevents removing the finalSUPER_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) withgranularity auto|day|monthand configurable currency (USD|EUR|GBP|EGP|SAR|AED). Vector tables + line/bar/pie charts via purepdfkit(no nativecanvas/Chromium), streamingapplication/pdfwithCache-Control: no-store,Content-Disposition: attachment|inlineandX-Report-Currency;?format=jsonreturns the same data for preview. Uses the same P&L aggregates as analytics (SUM(subtotal−discount),SUM(quantity×cost_price)COGS,operating_expensesbyspent_at) with zero-filled series. - Audit: Append-only
audit_logstable (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 authenticatedPOST/PUT/PATCH/DELETEon/admin/*plus business events (order placed, coupon redeemed). - Uploads:
GET /api/v1/uploads/imagekit-authandGET /api/v1/admin/products/uploads/imagekit-authissue 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:
- A customer registers through the storefront and verifies the account via the email link served by the API (
/verify-emailstatic page). - The customer logs in and receives an
httpOnlysession cookie, fetches a CSRF token, and browses products with search/filter/sort. - The customer chooses a size/color variant (live availability + computed final price) and adds it to a cart.
- The customer selects a saved address and optionally applies a coupon during checkout; shipping rules settle automatically.
- The API creates order snapshots, payment record, shipment, stock changes, and coupon usage as one transaction and clears the cart.
- The customer lists order history, opens order detail (immutable invoice), and submits a review with images.
- 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.tsassembles Express,trust proxy(envTRUST_PROXY),helmet, CORS (singleCORS_ORIGIN), JSON parsing, cookies,requestId, global rate limiter (skipped intest), CSRF (doubleCsrfbound to session), audit-log middleware on/admin/*, and the centralerrorHandlerwith 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_truncP&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 viasrc/config/env.tsand the process exits on missing/invalid config. - External adapters: Integrations sit behind small boundaries (
Mailer,SmsProvider,PaymentGatewayinterface +MockPaymentGateway,ImageKitsigned 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/**):
| Area | Responsibilities |
|---|---|
| Authentication | Register, login, logout, GET /auth/session + GET /auth/csrf-token, session list/revocation, verification (register), password reset via link + OTP |
| Users and addresses | Profiles, account deletion, password/email/phone change flows, saved shipping addresses (default flags, ownership-scoped) |
| Catalog | Products, variants, categories, product and variant images (ordered, one primary) |
| Inventory | Per-variant stock ledger with reserve/commit/release, manual admin adjustments and reservations |
| Cart | Single lazily-created cart: variant line items, quantities, clear |
| Orders | Customer checkout + order history/detail; admin order queue + FSM transitions (confirm/processing/ship/deliver/cancel/return/refund) |
| Reviews | Customer create/edit/delete own review, product reviews, my reviews; admin moderation |
| Coupons | Admin coupon CRUD, usage inspection, coupon_usages history |
| Reports | P&L Statement, Expenses, Revenue PDFs by `month |
| Administration | Role-protected management across domains; admin accounts (SUPER_ADMIN), stats, analytics (overview/coupons/expenses P&L), reports, audit log |
| Uploads | Signed 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_locktransaction lock and conditional stock-reservationUPDATE … WHERE available >= qty— overselling is structurally impossible. - All four cart mutations (
addCartItem,updateCartItemQuantity,removeCartItem,clearCart) route through the samewithUserCartLockadvisory lock so cart vs checkout races degrade to documented 404s, never 500s. - Inventory distinguishes
quantity_on_handandquantity_reservedand providesreserveStock/commitStock/releaseStockfor 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_usagesrow (unique onorders_id); pre-fulfillment cancellation restores quota (delete coupon_usages+ guardedusage_countdecrement), post-fulfillment refunds keep it consumed. - Order status follows an explicit
allowedTransitionsmap executed underSELECT … 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.
DELETEon admin-owned resources uses soft deletes (deleted_at) for ten tables; customer cart line removal is hard delete by variant public ID;audit_logsis append-only.
Media and Integrations
- ImageKit: An authenticated user or administrator requests short-lived HMAC upload parameters from
GET /api/v1/uploads/imagekit-authorGET /api/v1/admin/products/uploads/imagekit-auth(SDK v7.10.0ImageKit({ 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 insrc/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:
PaymentGatewayis an interface with aMockPaymentGatewayimplementation; the current checkout records mock payment references (pluggablePaymobintegration 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
| Technology | Purpose |
|---|---|
| Node.js and Express 5 | HTTP 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-pg | Typed database access using the PostgreSQL driver adapter |
| Zod | Runtime validation for environment configuration, shared passwordField, and every request's body/query/params |
bcrypt and nanoid | Password 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 |
| Resend | Transactional email delivery (verification, password reset, email change) |
| Pino | Structured request/application logging with request-ID correlation and secret redaction |
| pdfkit | Streaming application/pdf generation — vector tables + line/bar/pie charts, no native canvas/Chromium |
| Vitest 4 and Supertest | Unit, integration (real PG), and HTTP API testing (78 files / 1,132 tests) |
| Helmet, CORS, cookies, rate limiting | HTTP 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(postinstallrunsprisma generate). - Start:
npm run db:migrate:deploy && npm start— applies pending migrations, then bootsdist/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.exampleinto the platform secret store.CORS_ORIGINmust equal the public storefront origin (drives both CORS and everyCORS_ORIGIN-based email link);TRUST_PROXY=1(one proxy hop);SESSION_SECRETrotation kills all sessions;ENABLE_CSRFdefaults totrueand 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,
httpOnlycookies, 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 UPDATEand 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.
