# Sport Store A modern sports-fashion e-commerce platform. Built entirely in code — no WordPress, no WooCommerce, no Shopify, no CMS platform underneath. **Status: milestone 0 — architecture skeleton.** The structure, boundaries, data model and tooling are in place and verified. Business features are not implemented yet; see [Roadmap](#roadmap). ``` Storefront (Next.js) ─┐ ├─▶ REST API (NestJS) ─▶ PostgreSQL · Redis · R2/S3 Admin (Next.js) ──────┘ ``` --- ## Quick start Requires **Node ≥ 22**, **pnpm ≥ 10** (`corepack enable`) and **Docker**. ```bash pnpm setup # env files, real JWT secrets, deps, containers, migrations, seed pnpm dev # storefront + admin + API, all watching ``` | Service | URL | | ------------------ | --------------------------------------------------- | | Storefront | http://localhost:3000 | | Admin | http://localhost:3001 | | API | http://localhost:4000/api/v1/health | | API docs (Swagger) | http://localhost:4000/docs | | MinIO console | http://localhost:9001 (`sportminio` / `sportminio`) | | Mailpit | http://localhost:8025 | Verify the stack is healthy: ```bash curl -s http://localhost:4000/api/v1/health | jq ``` ```json { "success": true, "data": { "status": "ok", "dependencies": { "database": { "status": "up", "latencyMs": 3 }, "redis": { "status": "up", "latencyMs": 1 } } }, "meta": { "requestId": "019fef70-a80f-77d5-a975-bbe9f5f0c32c", "timestamp": "…" } } ``` > **Ports.** PostgreSQL is published on **5433** and Redis on **6380**, not their defaults. Many > machines already run one or both, and a shadowed port surfaces as a baffling authentication > error rather than a clear conflict. Inside the Docker network they still use 5432/6379. --- ## Commands ### Everyday ```bash pnpm dev # everything pnpm dev:storefront # one app (dependencies are built first) pnpm dev:admin pnpm dev:api pnpm build # build all, in dependency order pnpm lint # ESLint across the workspace pnpm lint:fix pnpm typecheck # tsc --noEmit everywhere pnpm test pnpm format # Prettier write pnpm format:check ``` ### Infrastructure ```bash pnpm infra:up # PostgreSQL, Redis, MinIO, Mailpit pnpm infra:down pnpm infra:logs pnpm infra:reset # destroys volumes, then restarts docker compose --profile full up --build # full stack behind Nginx on :80 ``` ### Database ```bash pnpm db:migrate # create + apply a migration (dev) pnpm db:deploy # apply pending migrations (production) pnpm db:generate # regenerate Prisma Client pnpm db:seed # reconcile permissions + roles (idempotent) pnpm db:studio # Prisma Studio pnpm db:reset # drop, re-migrate, re-seed ``` ### Running one app manually ```bash pnpm --filter @sport/api run dev # :4000 pnpm --filter @sport/storefront run dev # :3000 pnpm --filter @sport/admin run dev # :3001 ``` --- ## Repository layout ``` sport-store/ ├── apps/ │ ├── storefront/ Next.js customer site (:3000) │ │ └── src/ │ │ ├── app/ App Router — (shop) (checkout) (account) route groups │ │ ├── components/ Cross-feature UI (layout, chrome) │ │ ├── features/ auth · product · category · collection · search │ │ │ cart · checkout · order · wishlist · account │ │ ├── hooks/ lib/ services/ stores/ styles/ types/ │ │ │ ├── admin/ Next.js back office (:3001) │ │ └── src/ │ │ ├── app/ (auth) login · (dashboard) everything else │ │ ├── components/ features/ lib/ styles/ │ │ │ └── api/ NestJS modular monolith (:4000) │ ├── prisma/ schema.prisma · migrations · seed.ts │ └── src/ │ ├── config/ env validation → typed config object │ ├── common/ decorators · filters · guards · interceptors │ │ middleware · pipes · errors │ ├── infrastructure/ prisma · redis · storage · events · logging │ └── modules/ 20 bounded contexts │ ├── packages/ │ ├── types/ Framework-free domain + API contracts (zero deps) │ ├── validation/ Zod schemas shared by API and both frontends │ ├── api-client/ The only sanctioned way for a frontend to reach the API │ ├── ui/ Design-system primitives (Button, Input, Badge, Skeleton) │ ├── config/ Shared tsconfig bases + Tailwind theme tokens │ └── eslint-config/ Flat configs incl. the architectural boundary rules │ ├── infrastructure/ │ ├── docker/ One Dockerfile per app (turbo prune → standalone) │ ├── nginx/ Edge routing, rate ceiling, asset caching │ └── scripts/ bootstrap.sh · reset-db.sh │ ├── docs/ │ ├── architecture.md Boundaries, conventions, risks — read this first │ └── adr/ 12 decision records │ ├── docker-compose.yml Backing services; `--profile full` runs everything ├── turbo.json pnpm-workspace.yaml package.json ``` --- ## Architecture in brief Full detail in [`docs/architecture.md`](./docs/architecture.md). The rules that matter most: 1. **Only the API touches data.** Neither frontend has a database, Redis or storage client. Enforced by ESLint and by the absence of `DATABASE_URL` from their environments. ([ADR-0004](./docs/adr/0004-the-admin-dashboard-has-no-database-access.md)) 2. **All business logic lives in the backend.** The storefront may format `₫250.000`; it may never compute a discount. 3. **Product ≠ ProductVariant.** A product has a page; a _variant_ has a SKU, a price and stock. Every colour × size combination is its own variant. ([ADR-0003](./docs/adr/0003-product-and-productvariant-as-separate-entities.md)) 4. **Authorization is permissions, never role checks.** There is no `if (user.role === 'ADMIN')` anywhere. ([ADR-0007](./docs/adr/0007-rbac-permissions-instead-of-role-checks.md)) 5. **Authentication is on by default.** The access-token guard is global; a route is public only by declaring `@Public()`. Forgetting a decorator fails closed. 6. **Money is an integer in minor units**, everywhere. ([ADR-0011](./docs/adr/0011-money-as-integer-minor-units.md)) 7. **Modules own their tables exclusively.** Cross-module access goes through a `public/` barrel or a domain event — enforced by ESLint, which is what keeps a future service extraction possible. ([ADR-0002](./docs/adr/0002-modular-monolith-not-microservices.md)) ### Storefront routes `/` · `/men` · `/women` · `/sports/[running|football|training|gym|badminton|lifestyle]` · `/products/[slug]` · `/collections/[slug]` · `/search` · `/cart` · `/checkout` · `/account/[profile|orders|addresses|wishlist]` · `/blog` ### Backend modules `auth` `users` `customers` `products` `product-variants` `categories` `collections` `brands` `inventory` `carts` `checkout` `orders` `payments` `promotions` `coupons` `wishlist` `reviews` `cms` `media` `search` (+ `health`) --- ## Roadmap | Milestone | Scope | | --------- | ------------------------------------------------------------------------------------- | | **M0** ✅ | Architecture, tooling, schema, health check, Docker, CI | | **M1** | Catalog read API: products, variants, categories, collections, brands + Redis caching | | **M2** | Auth: login, refresh rotation, RBAC admin, user/role management | | **M3** | Admin catalog: product editor, variant matrix, media uploads, inventory | | **M4** | Storefront catalog: listings, PDP with variant selector, filters | | **M5** | Cart, checkout, orders | | **M6** | Search + faceting | | **M7** | Promotions, coupons, reviews, CMS | | **M8** | Customer account | | **M9** | Payments (VNPay, MoMo, ZaloPay, COD), shipping, notifications | **Recommended next step: M1.** It exercises every layer end to end — Prisma repository → service → controller → envelope → `@sport/api-client` → a rendered page — on read-only endpoints where mistakes are cheap. It also proves the variant model against real data before anything writes to it. Auth (M2) comes second because the enforcement half already exists; only issuance is missing. --- ## Verified in this environment Everything below was run, not assumed: - `pnpm install` — 10 workspace projects resolved - `pnpm lint` · `pnpm typecheck` · `pnpm build` — 24/24 Turborepo tasks pass - `pnpm format:check` — clean - `prisma migrate dev` — 25 tables created - `pnpm db:seed` — 36 permissions, 6 roles - API boots; `GET /api/v1/health` returns `status: ok` with PostgreSQL and Redis both `up` - Error envelope confirmed on a 404; `x-request-id` echoed; Helmet, CORS and rate-limit headers present; Swagger served at `/docs` - `pnpm test` — 5 passing RBAC guard tests - Storefront renders 20 routes (`/sports/curling` correctly 404s); admin renders 17 Known benign noise: NestJS logs two `Unsupported route path: "/api/*"` warnings at boot. They come from Nest's own global-prefix handling under Express 5 / path-to-regexp v8, are auto-converted correctly, and routing is verified working. Nothing in this repository registers that path. --- ## Contributing - Branches: `feat/…`, `fix/…`, `chore/…`, `docs/…` - Commits: [Conventional Commits](https://www.conventionalcommits.org/) - CI runs lint, typecheck, format, tests against real PostgreSQL and Redis, builds every app, and builds all three Docker images on push. - Architectural changes need an ADR in [`docs/adr/`](./docs/adr/README.md).