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.
| 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 |
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
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
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.
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
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
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
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 >= requestedQuantityRedis locks reduce contention; PostgreSQL transactions enforce stock integrity.
Detailed docs: CART_ARCHITECTURE.md, INVENTORY_RESERVATION.md
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
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
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"]
Supported event contracts:
OrderPlacedPaymentCompletedInventoryReservedCartExpiredProductUpdated
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
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"]
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
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
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.
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
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
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"]
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
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"]
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
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
Prerequisites:
- Node.js 22+ or 24+
- pnpm through Corepack
- Docker Desktop
- Supabase PostgreSQL project
Install dependencies:
corepack enable
corepack pnpm installCreate .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/webhookCopy 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:checkOne-command local startup:
corepack pnpm local:startThis 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
Install:
corepack pnpm installGenerate Prisma Client:
corepack pnpm db:generateApply migrations:
corepack pnpm db:migrateSeed Supabase:
corepack pnpm db:seedRun app services in Docker:
corepack pnpm docker:upRun one-shot seed in Docker:
corepack pnpm docker:seedFollow logs:
corepack pnpm docker:logsRun health checks:
corepack pnpm healthcheckRebuild images:
corepack pnpm docker:rebuildShutdown:
corepack pnpm docker:downLocal source hot reload is available through the workspace dev command:
corepack pnpm devFor hot reload, run Redis and Meilisearch first:
corepack pnpm infra:upDocker Compose starts these local services:
web: Next.js standalone frontendapi: Fastify APIworker: BullMQ worker runtimeemail-service: internal Resend-backed transactional email webhookredis: cache, locks, queues, rate limitsmeilisearch: product/category searchmigrate: one-shot Prisma migration job
Supabase PostgreSQL is external and is not started by Docker Compose.
Startup ordering:
- Redis and Meilisearch must be healthy.
migratemust 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.
Automated healthcheck:
corepack pnpm healthcheckThe 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 pingAfter 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>"}'Environment validation fails:
- Check
.envexists. - Replace all placeholder Supabase values.
- Ensure JWT secrets are at least 32 characters.
- Keep
DIRECT_URLon a direct or session-safe Supabase connection.
Prisma migrate fails:
- Verify
DIRECT_URL. - Avoid Supabase transaction pooler URLs for migrations.
- Run
corepack pnpm db:generatebefore 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
6379is 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_SECRETmatches the lateststripe listensecret. - Check API logs for
WEBHOOK RECEIVED,PAYMENT UPDATED,ORDER UPDATED, andREVENUE UPDATED.
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
- System Design
- Monorepo Architecture
- Database Architecture
- Auth Architecture
- API Security
- Cache Architecture
- Async Jobs
- Observability
- Product Architecture
- Cart Architecture
- Inventory Reservation
- Payment Architecture
- Order Architecture
- Search Architecture
- Event-Driven Architecture
- Testing Strategy
- Deployment and Infrastructure