Skip to content

About

Production-grade event-driven ecommerce platform built with Next.js, Fastify, Prisma, Redis, BullMQ, Meilisearch, Stripe, and Docker.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

55 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Scalable Ecommerce Platform

A production-grade ecommerce backend and platform architecture built as a strict TypeScript monorepo. The project is designed to demonstrate startup-ready backend engineering: modular Fastify services, PostgreSQL/Prisma data modeling, Redis-backed distributed coordination, BullMQ workers, Meilisearch discovery, event-driven workflows, secure auth/payment patterns, and Docker-based deployment.

This is not a toy storefront. The repository focuses on the engineering foundation behind a scalable commerce system: consistency boundaries, operational safety, async processing, typed contracts, observability, and maintainable architecture.

Tech Stack

Layer Technology
Monorepo pnpm workspaces, Turborepo
Frontend Next.js, React, TypeScript
API Fastify, TypeScript, Zod, Pino
Data PostgreSQL, Prisma
Cache and coordination Redis
Async jobs BullMQ
Search Meilisearch
Auth JWT access tokens, rotating refresh tokens, HTTP-only cookies, RBAC
Payments Stripe Checkout, signed Stripe webhooks, COD admin payment capture
Observability Structured logs, request/correlation IDs, Sentry-ready error tracking
Testing Vitest, Supertest
Infrastructure Docker, Docker Compose, GitHub Actions, GHCR

Repository Structure

apps/
  web/                  Next.js frontend
  api/                  Fastify API service
  worker/               BullMQ worker runtime
  email-service/        Internal transactional email service backed by Resend

packages/
  cache/                Redis cache, cart storage, inventory locks
  config/               Shared strict TypeScript/env configuration
  database/             Prisma schema and database package boundary
  events/               Typed domain event contracts and factories
  logger/               Structured logging foundation
  queue/                BullMQ queue names, schemas, routing, idempotency
  search/               Meilisearch client, documents, filters, ranking config
  types/                Shared DTO/type package
  ui/                   Shared frontend UI package
  validation/           Zod parsing and validation helpers

prisma/                 Root Prisma entrypoint mirror
.github/workflows/     CI, image publishing, deployment

System Overview

flowchart LR
  Browser["Web Client"] --> Web["Next.js Web"]
  Web --> API["Fastify API"]

  API --> PG[("PostgreSQL")]
  API --> Redis[("Redis")]
  API --> Search[("Meilisearch")]
  API --> Queue["BullMQ Queues"]

  Queue --> Worker["Worker Runtime"]
  Worker --> EmailSvc["Email Service"]
  Worker --> PG
  Worker --> Redis
  Worker --> Search
  EmailSvc --> Resend["Resend"]

  Payments["Stripe Checkout"] --> API
  API --> Payments
Loading

The API owns synchronous request validation, auth, state transitions, and durable writes. Redis handles cache, locks, rate limits, carts, and queues. Workers process expensive or eventually consistent workflows: invoice generation, analytics, search indexing, payment retries, inventory cleanup, and domain-event reactions.

Request Lifecycle

sequenceDiagram
  participant Client
  participant API as Fastify API
  participant Zod as Zod Validation
  participant Service as Domain Service
  participant DB as PostgreSQL
  participant Redis
  participant Queue as BullMQ
  participant Worker

  Client->>API: HTTP request
  API->>API: requestId/correlationId
  API->>Zod: validate params/query/body
  API->>Service: execute business operation
  Service->>Redis: cache/lock/rate-limit when needed
  Service->>DB: transactional state change
  Service->>Queue: enqueue async follow-up
  API-->>Client: typed response
  Queue-->>Worker: background job
  Worker->>DB: async state/read-model update
Loading

Core Backend Domains

Authentication and Authorization

Authentication uses short-lived JWT access tokens and rotating refresh tokens stored in HTTP-only cookies. Sessions are persisted for revocation and device tracking. RBAC middleware centralizes route authorization so business modules do not duplicate permission checks.

Security properties:

  • no localStorage token storage
  • refresh token rotation
  • session invalidation
  • CSRF strategy for cookie-backed refresh flows
  • centralized error handling and validation formatting

Detailed doc: AUTH_ARCHITECTURE.md

Product, Search, and Catalog

Products are modeled with categories, variants, images, inventory, and SEO-friendly slugs. Search is handled by Meilisearch rather than SQL LIKE queries. Product documents are denormalized for fast filtering and typo-tolerant discovery.

Product detail pages also surface verified-purchase reviews, average rating, review count, and star breakdowns. Reviews can only be created from delivered, paid order items, so anonymous or unfulfilled-order reviews are rejected.

Search ranking prioritizes textual relevance first, then business signals:

  • popularity
  • availability
  • recency

Detailed docs: PRODUCT_ARCHITECTURE.md, SEARCH_ARCHITECTURE.md

Cart and Inventory

Carts use Redis for fast interaction and expiration while retaining a path to database persistence and recovery. Inventory reservations prevent overselling by combining Redis locks with PostgreSQL conditional updates.

The correctness boundary is the database, not the cache:

quantity - reserved - safetyStock >= requestedQuantity

Redis locks reduce contention; PostgreSQL transactions enforce stock integrity.

Detailed docs: CART_ARCHITECTURE.md, INVENTORY_RESERVATION.md

Orders and Payments

Orders and payments are tracked separately. Payment state drives revenue; order state drives fulfillment. The platform does not trust frontend success redirects for online payments.

stateDiagram-v2
  [*] --> pending
  pending --> confirmed
  pending --> cancelled
  confirmed --> paid
  confirmed --> cancelled
  pending --> paid
  paid --> delivered
  paid --> refunded
  delivered --> refunded
Loading

Stripe Checkout is the sole online payment provider. Cards, UPI, and wallet availability are controlled by Stripe account/payment-method support. Signed Stripe webhooks update local payment state, mark successful Stripe orders paid, consume inventory reservations, and trigger payment-success email workflows.

COD orders are created as pending and become paid only when an admin uses Mark Paid. Admins can also mark paid orders delivered, which records delivery time and triggers delivery confirmation email. Revenue metrics count paid payments only, not pending, failed, or cancelled orders.

Buy Now is isolated from the cart lifecycle. It creates a standalone checkout session and must not mutate the active cart.

Delivered orders unlock verified-purchase review actions for purchased products. The delivered-order email includes review links, and product rating aggregates are recalculated transactionally when reviews are created, deleted, or moderated.

Detailed docs: ORDER_ARCHITECTURE.md, PAYMENT_ARCHITECTURE.md

Event-Driven Workflows

Domain events decouple producers from downstream reactions.

flowchart TD
  Domain["Domain Service"] --> EventLog[("DomainEventLog")]
  Domain --> EventQueue["domain-event.dispatch"]
  EventQueue --> Consumer["Worker Consumer"]

  Consumer --> Invoice["Invoice Job"]
  Consumer --> Analytics["Analytics Job"]
  Consumer --> SearchIndex["Search Index Job"]
  Consumer --> Recovery["Recovery/Reconciliation Jobs"]
Loading

Supported event contracts:

  • OrderPlaced
  • PaymentCompleted
  • InventoryReserved
  • CartExpired
  • ProductUpdated

Each event includes tenant, aggregate, correlation, causation, and schema version metadata. Consumers are expected to be idempotent, and queue job IDs are derived from event or aggregate identity.

Detailed doc: EVENT_DRIVEN_ARCHITECTURE.md

Queue Architecture

BullMQ powers async processing with typed job schemas, retry policies, and dead-letter concepts.

flowchart LR
  API["API"] --> Queue["BullMQ"]
  Queue --> Email["email"]
  Email --> Worker["Worker"]
  Worker --> EmailSvc["Internal Email Service"]
  EmailSvc --> Resend["Resend"]
  Queue --> Invoice["invoice"]
  Queue --> Analytics["analytics"]
  Queue --> Payments["payment-retry"]
  Queue --> Inventory["inventory"]
  Queue --> Search["search"]
  Queue --> Events["domain-events"]
  Queue --> DLQ["dead-letter"]
Loading

Async workloads include:

  • order confirmation emails
  • payment success emails
  • delivery confirmation emails with review CTAs
  • invoice generation
  • analytics
  • payment retries
  • inventory reservation cleanup
  • stock synchronization
  • search indexing
  • domain event dispatch

Detailed doc: ASYNC_JOBS.md

Email Service

Transactional emails are delivered through an internal monorepo service instead of calling the email provider directly from API request handlers.

sequenceDiagram
  participant API
  participant Queue as BullMQ
  participant Worker
  participant Email as Email Service
  participant Resend
  participant User

  API->>Queue: enqueue email.send
  Queue->>Worker: process job
  Worker->>Email: POST /send with x-email-secret
  Email->>Email: validate payload and render template
  Email->>Resend: send transactional email
  Resend-->>User: deliver email
Loading

The worker preserves queue retries and idempotency keys. The email service validates x-email-secret, renders typed templates, and sends through Resend. Current templates cover order confirmation, payment success, delivery confirmation, and review CTA emails.

Caching Strategy

Redis is used deliberately, not as a blanket replacement for the database.

Use case Strategy
Product/category reads read-through cache with TTL
Hot products short TTL cache
Carts Redis-backed mutable cart state with expiration
Inventory Redis locks plus database conditional writes
Rate limiting tenant/IP scoped counters
Queues BullMQ transport

Cache invalidation is namespaced by tenant and domain. Product/catalog changes should emit domain events and enqueue search/cache refresh workflows.

Detailed doc: CACHE_ARCHITECTURE.md

Database Design

The PostgreSQL schema is tenant-scoped and normalized around ecommerce boundaries:

  • tenants, users, sessions, RBAC
  • products, categories, variants, images
  • carts and cart items
  • inventory items and reservations
  • orders and order events
  • payments, provider references, paid timestamps, and webhook inbox
  • verified-purchase reviews and denormalized product rating aggregates
  • audit logs
  • domain event log

Indexing focuses on tenant-scoped access patterns, lifecycle queries, cursor pagination, cleanup jobs, and high-frequency lookup fields.

Detailed doc: DATABASE_ARCHITECTURE.md

Observability

Operational visibility is built around structured logs and correlation IDs.

flowchart LR
  Request["HTTP Request"] --> Context["requestId / correlationId"]
  Context --> Logs["Pino Structured Logs"]
  Context --> Errors["Sentry Error Tracking"]
  Context --> Jobs["Queue Job Metadata"]
  Context --> Events["Domain Event Metadata"]
Loading

The API attaches correlation data to requests, logs slow APIs, formats errors consistently, and carries request metadata into queue jobs and domain events.

Detailed doc: OBSERVABILITY.md

Infrastructure and Deployment

The platform ships with Docker and Docker Compose foundations for local and production-style deployment.

flowchart TD
  GitHub["GitHub"] --> CI["CI: typecheck, tests, Prisma validate"]
  CI --> Publish["Build and publish Docker images"]
  Publish --> GHCR["GHCR"]
  GHCR --> Deploy["Deploy workflow"]
  Deploy --> Host["Docker Compose Host"]

  Host --> Web["web"]
  Host --> API["api"]
  Host --> Worker["worker"]
  Host --> Email["email-service"]
  Host --> Supabase["Supabase Postgres"]
  Host --> Redis["redis"]
  Host --> Meili["meilisearch"]
Loading

Included:

  • local full-stack docker-compose.yml
  • production image-based docker-compose.prod.yml
  • app Dockerfiles
  • health checks
  • environment templates
  • GitHub Actions CI
  • GHCR image publishing
  • manual deployment workflow

Detailed doc: DEPLOYMENT_INFRASTRUCTURE.md

Testing Strategy

The testing foundation uses Vitest and Supertest.

Coverage slices include:

  • auth registration/session behavior
  • payment webhook verification
  • inventory reservation locking
  • cart merge behavior
  • order state-machine rules
  • queue schema/routing/idempotency
  • API health and error formatting

Integration tests are intentionally gated so local test runs stay fast and deterministic.

Detailed doc: TESTING_STRATEGY.md

Local Setup

Prerequisites:

  • Node.js 22+ or 24+
  • pnpm through Corepack
  • Docker Desktop
  • Supabase PostgreSQL project

Install dependencies:

corepack enable
corepack pnpm install

Create .env from .env.example, then replace the Supabase database values and Stripe values:

DATABASE_URL="postgresql://prisma.<project-ref>:<password>@<region>.pooler.supabase.com:5432/postgres?schema=public&sslmode=require"
DIRECT_URL="postgresql://postgres.<project-ref>:<password>@<region>.pooler.supabase.com:5432/postgres?schema=public&sslmode=require"

DATABASE_URL is used by API, worker, seed, and Prisma Client runtime. DIRECT_URL is used by Prisma CLI migration commands. Keep DIRECT_URL on a direct or session-safe Supabase connection so migrations do not run through the transaction pooler.

Stripe local testing requires:

STRIPE_SECRET_KEY="sk_test_xxx"
STRIPE_WEBHOOK_SECRET="whsec_xxx"
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_xxx"

Forward Stripe webhooks locally:

stripe listen --forward-to localhost:4000/payments/stripe/webhook

Copy the generated whsec_... value into STRIPE_WEBHOOK_SECRET. Without this webhook forwarding, Stripe Checkout can succeed while local orders remain pending.

Email delivery requires Resend and the internal email-service secret:

EMAIL_WEBHOOK_URL="http://localhost:4100/send"
EMAIL_SERVICE_SECRET="replace-with-at-least-32-characters-email-secret"
RESEND_API_KEY="re_xxx"
EMAIL_FROM="Store <orders@example.com>"

For Railway or another host, deploy apps/email-service independently and set EMAIL_WEBHOOK_URL in the worker to the deployed /send endpoint.

Validate local environment:

corepack pnpm env:check

One-command local startup:

corepack pnpm local:start

This command validates .env, builds and starts Docker Compose, applies migrations through the migrate service, runs the seed job, and verifies the frontend, backend, Redis, Meilisearch, and Prisma-backed readiness endpoint.

Useful endpoints:

  • Web: http://localhost:3000
  • API health: http://localhost:4000/health
  • API readiness: http://localhost:4000/health/ready
  • Email service health: http://localhost:4100/health
  • Meilisearch: http://localhost:7700/health

Local Commands

Install:

corepack pnpm install

Generate Prisma Client:

corepack pnpm db:generate

Apply migrations:

corepack pnpm db:migrate

Seed Supabase:

corepack pnpm db:seed

Run app services in Docker:

corepack pnpm docker:up

Run one-shot seed in Docker:

corepack pnpm docker:seed

Follow logs:

corepack pnpm docker:logs

Run health checks:

corepack pnpm healthcheck

Rebuild images:

corepack pnpm docker:rebuild

Shutdown:

corepack pnpm docker:down

Local source hot reload is available through the workspace dev command:

corepack pnpm dev

For hot reload, run Redis and Meilisearch first:

corepack pnpm infra:up

Docker Setup

Docker Compose starts these local services:

  • web: Next.js standalone frontend
  • api: Fastify API
  • worker: BullMQ worker runtime
  • email-service: internal Resend-backed transactional email webhook
  • redis: cache, locks, queues, rate limits
  • meilisearch: product/category search
  • migrate: one-shot Prisma migration job

Supabase PostgreSQL is external and is not started by Docker Compose.

Startup ordering:

  • Redis and Meilisearch must be healthy.
  • migrate must complete successfully.
  • API and email-service start after migrations and infrastructure are ready.
  • Worker starts after migrations, infrastructure, and email-service are ready.
  • Web starts after API is healthy.

Healthchecks

Automated healthcheck:

corepack pnpm healthcheck

The script validates:

  • frontend responds on localhost:3000
  • backend responds on localhost:4000/health
  • Prisma database readiness through localhost:4000/health/ready
  • email service responds on localhost:4100/health
  • Redis responds to PING
  • Meilisearch reports available

Manual checks:

curl http://localhost:3000
curl http://localhost:4000/health
curl http://localhost:4000/health/ready
curl http://localhost:7700/health
docker compose --env-file .env exec redis redis-cli ping

Meilisearch Bootstrap

After login with an admin user, initialize indexes:

curl -X POST http://localhost:4000/search/admin/setup-indexes \
  -H "Authorization: Bearer <accessToken>"

To rebuild search documents:

curl -X POST http://localhost:4000/search/admin/rebuild \
  -H "Authorization: Bearer <accessToken>" \
  -H "Content-Type: application/json" \
  -d '{"tenantId":"<tenantId>"}'

Troubleshooting

Environment validation fails:

  • Check .env exists.
  • Replace all placeholder Supabase values.
  • Ensure JWT secrets are at least 32 characters.
  • Keep DIRECT_URL on a direct or session-safe Supabase connection.

Prisma migrate fails:

  • Verify DIRECT_URL.
  • Avoid Supabase transaction pooler URLs for migrations.
  • Run corepack pnpm db:generate before retrying.

API readiness says database is down:

  • Verify DATABASE_URL.
  • Confirm Supabase allows connections from your network.
  • Check API logs with corepack pnpm docker:logs.

Redis is down:

  • Run corepack pnpm infra:up.
  • Check port 6379 is not already owned by another local Redis.

Web is unhealthy:

  • Rebuild with corepack pnpm docker:up.
  • Confirm NEXT_PUBLIC_API_URL=http://localhost:4000.
  • Check that API is healthy before debugging the frontend.

Stripe payment succeeds but order remains pending:

  • Confirm Stripe CLI is forwarding to localhost:4000/payments/stripe/webhook.
  • Verify STRIPE_WEBHOOK_SECRET matches the latest stripe listen secret.
  • Check API logs for WEBHOOK RECEIVED, PAYMENT UPDATED, ORDER UPDATED, and REVENUE UPDATED.

Engineering Tradeoffs

This project optimizes for maintainability and operational clarity over premature complexity.

Intentional choices:

  • Fastify modules instead of a monolithic API file.
  • PostgreSQL as source of truth.
  • Redis for cache/coordination, not durable business state.
  • BullMQ for async workflows before introducing Kafka/NATS.
  • Meilisearch for product discovery instead of database text search.
  • Docker Compose as a pragmatic deployment target before Kubernetes/ECS.
  • Shared type packages for contracts across API, worker, and frontend.

Known next hardening steps:

  • wire a concrete app-level Prisma client plugin into all production repositories
  • switch internal package exports from TypeScript source to built dist
  • add migration execution to deployment
  • add worker heartbeat health checks
  • add replay tooling for DomainEventLog
  • add full integration tests against real Postgres/Redis/Meilisearch

Design Documents

About

Production-grade event-driven ecommerce platform built with Next.js, Fastify, Prisma, Redis, BullMQ, Meilisearch, Stripe, and Docker.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages