This is a production-ready online store I built with Claude to learn Typescript, React and explore a couple of tools and concepts related to Redis, Docker, Stripe, MailHog, AWS and Github Actions. Claude has filled some gaps that were needed to make this production-ready, but I've focused primarily on those tools.
It's a full e-commerce platform: customers can browse a catalog, add items to a cart, register / log in, check out via Stripe, and view their order history. Admins can manage products, categories, inventory and images from a role-gated admin panel.
- What this app does
- Live demo flow
- Tech stack
- Architecture at a glance
- Repo layout
- Quick start (local dev)
- Stripe setup
- How to use the app
- Available scripts
- Testing
- Deploying to AWS
- Documentation index
- License
I've tried to solve both the customer facing and merchant facing sides:
- Browse the catalog — search by keyword, filter by category, sort by price/newest, paginate. Lazy-loaded images, mobile-friendly grid.
- Product detail pages — image gallery, full description, stock level, "Add to cart" with optimistic UI updates.
- Shopping cart — works for both anonymous guests (cookie-backed) and logged-in users (DB-backed). On login, the guest cart is automatically merged into the user account.
- Account management — signup with email verification, login, password reset, profile, shipping addresses, order history.
- Checkout — Stripe-hosted checkout collects payment securely; we never see card numbers. Orders are created server-side via a signed webhook, with stock decremented inside a database transaction so the same item can't be oversold.
- Order history — list view + detail page showing snapshot of items, prices, status, and shipping address at time of purchase.
- Product CRUD — create, edit, deactivate (soft-delete). Unique slugs, price in cents, stock, category assignment.
- Image upload — direct browser-to-S3 upload using presigned URLs. Multiple images per product, ordered.
- Category CRUD — name + slug + optional description. Blocks deletion if products still reference the category.
- Audit log — every admin write goes to an
AuditLogtable: who-did-what-when, with before/after snapshots.
- Production-grade auth — JWT access tokens (15 min) + rotating refresh tokens (httpOnly, Secure cookies, 30 days), bcrypt password hashing.
- CSRF protection — Origin/Referer allowlist on cookie-mutating endpoints.
- Rate limiting — Redis-backed, shared across multiple API instances.
- Idempotent webhooks — Stripe events deduped via
(provider, eventId)unique constraint; safe to retry. - Observability — Pino structured logs with request IDs, Sentry error
tracking, Prometheus
/metricsendpoint exposing RED metrics + Node defaults. - Threat model — documented OWASP Top 10 checklist + mitigations.
- AWS deployment — Terraform modules for VPC, RDS, ElastiCache, ECS
Fargate, ALB, CloudFront, S3, WAF, Secrets Manager. Separate
stagingandprodenvironments with different sizing. - CI/CD — GitHub Actions: lint, typecheck, test, build, Trivy security scan on every PR; auto-deploy to staging on merge; gated deploy to prod on git tag.
After setup, here's what works end-to-end:
- Open http://localhost:5173 → home page with "Browse products" CTA
- Click Products → browse catalog (30 sample products in 6 categories)
- Use the search box, category filter, and sort dropdown — URL updates so you can share / bookmark
- Click a product → detail page with image gallery + Add to cart
- Add to cart (works as guest — cookie holds your cart)
- Click Sign in → log in with
customer@example.com/CustomerDemo1! - Your guest cart is automatically merged into your account
- Click Cart → adjust quantities, remove items, see live subtotal
- Proceed to checkout → shipping form → Stripe redirect (needs Stripe keys; see below)
- After payment → order appears at Orders within seconds (via webhook)
- Log out, log back in as
admin@example.com/AdminChangeMe1! - Header now shows an Admin link → product list, create/edit forms, image upload, category management
| Layer | Choice | Why |
|---|---|---|
| Frontend | React 18 + TypeScript + Vite | Modern, fast HMR, mature ecosystem |
| Routing | React Router v6 | Standard for SPAs |
| Server state | TanStack Query | Best-in-class caching + retries |
| Forms | React Hook Form + Zod | Fewer re-renders + type-safe validation |
| Styling | Tailwind CSS | Tiny output, no naming bikeshed |
| Backend | Node 20 + Fastify | Faster than Express, schema-first |
| Logging | Pino | Fastest structured logger |
| Database | PostgreSQL 16 + Prisma ORM | Battle-tested + type-safe queries |
| Cache | Redis 7 | Rate limit + session store + idempotency |
| Storage | S3-compatible (MinIO local / S3 prod) | Direct browser uploads via presigned URLs |
| nodemailer (Mailhog local / SES prod) | Standard SMTP everywhere | |
| Payments | Stripe Checkout + Webhooks | PCI scope minimized to SAQ-A |
| Auth | JWT + rotating refresh cookie + bcrypt | Short access tokens, revokable refresh |
| Infra | Terraform → AWS (ECS Fargate, RDS, ElastiCache, ALB, CloudFront, S3, WAF) | Standard, reproducible |
| CI/CD | GitHub Actions + AWS OIDC | No long-lived AWS keys in GitHub |
| Observability | Sentry + Prometheus + CloudWatch | Errors + metrics + logs |
| Tests | Vitest (unit/integration), Playwright (e2e), k6 (load) | Right tool per layer |
| Container | Docker (multi-stage, distroless runtime) | Same artifact local + prod |
A pnpm monorepo holds it all together, so the frontend and backend can share TypeScript types and Zod validators — there's a single source of truth for "what does a Product look like."
[browser :5173] ─► Vite dev server ─► proxies /api/* ─► [Fastify API :3001]
│
├─► Postgres :5432 (Docker)
├─► Redis :6379 (Docker)
├─► MinIO :9000 (Docker, pretend S3)
└─► Mailhog :1025 (Docker, pretend SES)
[browser]
│ HTTPS
▼
CloudFront + WAF
├─ / → S3 (React SPA static files)
├─ /images/* → S3 (product images)
└─ /api/* → ALB → ECS Fargate (Fastify API)
│
├─► RDS Postgres 16 (Multi-AZ, encrypted)
├─► ElastiCache Redis (primary + replica)
├─► S3 (presigned image uploads)
├─► SES (transactional email)
└─► Stripe (checkout sessions + webhooks)
Full Mermaid diagram + cost estimate in
docs/aws-architecture.md.
shopping-app/
├── apps/
│ ├── api/ # Fastify API server (Node)
│ │ ├── src/ # routes, auth, cart, orders, observability
│ │ ├── prisma/ # schema + migrations + seed
│ │ ├── test/ # vitest integration tests
│ │ ├── Dockerfile # multi-stage, distroless runtime image
│ │ └── .env.example
│ │
│ └── web/ # React SPA (Vite)
│ ├── src/
│ │ ├── pages/ # route components
│ │ ├── pages/admin/ # admin-only pages
│ │ ├── components/ # reusable UI bits
│ │ ├── auth/ # AuthContext + ProtectedRoute
│ │ └── lib/ # typed API clients
│ ├── Dockerfile # builds bundle, serves with nginx
│ ├── nginx.conf
│ └── .env.example
│
├── packages/
│ └── shared/ # Zod schemas + Money type + shared utils
│ └── src/schemas/ # auth, cart, order, product, admin
│
├── e2e/ # Playwright browser tests
│ └── tests/smoke.spec.ts # golden path: browse → cart → login → checkout
│
├── load-tests/
│ └── smoke.js # k6 smoke profile with SLO thresholds
│
├── infra/
│ └── terraform/
│ ├── bootstrap/ # one-time: state bucket + lock table + OIDC role
│ ├── modules/ # reusable: network, rds, redis, ecs-*, s3-cdn, waf, secrets
│ └── envs/{staging,prod}/ # per-environment wiring
│
├── docs/
│ ├── architecture.md # system overview + request lifecycle
│ ├── aws-architecture.md # production AWS topology + cost
│ ├── security.md # threat model + OWASP checklist
│ ├── launch-checklist.md # go-live checklist (60+ items)
│ ├── runbooks/ # deploy, incident response
│ └── adr/ # Architecture Decision Records
│
├── .github/
│ ├── workflows/
│ │ ├── ci.yml # lint, typecheck, test, audit, build, Docker scan
│ │ └── deploy.yml # PR plan + main→staging + tag→prod
│ └── dependabot.yml
│
├── docker-compose.yml # local: postgres + redis + minio + mailhog
├── pnpm-workspace.yaml # monorepo configuration
└── tsconfig.base.json # shared strict TS settings
| Tool | Min version | macOS install |
|---|---|---|
| Node.js | 20.11 | brew install node@20 && brew link --overwrite node@20 (or use nvm) |
| pnpm | 9 | brew install pnpm |
| Docker Desktop | recent | brew install --cask docker-desktop, then launch the app once |
Node 22/25 work for local dev. CI pins Node 20.
# 1. Get dependencies
pnpm install
# 2. Start backing services (Postgres, Redis, MinIO, Mailhog)
docker compose up -d
docker compose ps # all four should say "healthy"
# 3. Copy env files (gitignored copies of the *.example files)
cp apps/api/.env.example apps/api/.env
cp apps/web/.env.example apps/web/.env
# 4. Create the database schema + seed sample data
pnpm db:migrate:dev # creates tables
pnpm db:seed # 6 categories + 30 products + 2 users
# 5. Run everything (api + web with hot reload)
pnpm devOpen http://localhost:5173.
| Role | Password | |
|---|---|---|
| Admin | admin@example.com |
AdminChangeMe1! |
| Customer | customer@example.com |
CustomerDemo1! |
| What | URL |
|---|---|
| Web app | http://localhost:5173 |
| API health probe | http://localhost:3001/healthz |
| API readiness (pings DB + Redis) | http://localhost:3001/readyz |
Prometheus metrics (if METRICS_ENABLED=true) |
http://localhost:3001/metrics |
| Prisma Studio (browse DB visually) | pnpm --filter @shopping-app/api db:studio → http://localhost:5555 |
| MinIO console (pretend S3) | http://localhost:9001 · login minioadmin / minioadmin |
| Mailhog UI (catches outgoing email) | http://localhost:8025 |
docker compose down # stop services, keep data
docker compose down -v # stop AND delete all data (fresh start next time)The cart and order routes work without Stripe, but /checkout/session needs
real test-mode keys. Free, takes ~5 minutes.
- Sign up at https://dashboard.stripe.com/register. After signup, ensure the toggle in the top-right says "Test mode" (yellow banner).
- Grab your Secret key from
https://dashboard.stripe.com/test/apikeys (starts with
sk_test_...) and put it inapps/api/.env:STRIPE_SECRET_KEY=sk_test_51... - Install the Stripe CLI and have it forward webhooks to your local API:
It prints a
brew install stripe/stripe-cli/stripe stripe login stripe listen --forward-to localhost:3001/webhooks/stripe
whsec_...— paste it intoapps/api/.env:LeaveSTRIPE_WEBHOOK_SECRET=whsec_...stripe listenrunning while you test — it forwards real Stripe events to your local API so order creation actually happens. - Restart
pnpm devso the API picks up the new env vars. - In the browser: add a product to cart → Proceed to checkout → fill in the address → you'll redirect to Stripe's hosted page.
- Use card
4242 4242 4242 4242, any future expiry (12/34), any CVC. - You'll land on
/checkout/successand the order appears at/orderswithin 1–2 seconds.
| Goal | Path |
|---|---|
| Browse products | /products (search, filter, sort, paginate) |
| See a product | /products/<slug> |
| Sign up | /signup → check Mailhog at http://localhost:8025 for the verify link |
| Log in | /login |
| Reset password | /forgot-password → check Mailhog |
| View cart | /cart |
| Checkout | /checkout (requires login) |
| Order history | /orders |
| Single order | /orders/<id> |
| Account profile + logout | /account |
Log in as admin@example.com → an Admin link appears in the header.
| Goal | Path |
|---|---|
| List all products (incl. inactive) | /admin/products |
| Create a product | /admin/products/new |
| Edit a product + manage its images | /admin/products/<id> |
| Manage categories | /admin/categories |
A non-admin visiting /admin gets Access denied.
Run from the repo root.
| Command | What it does |
|---|---|
pnpm dev |
Run api + web concurrently with hot reload |
pnpm build |
Production build of every workspace |
pnpm typecheck |
tsc --noEmit across all workspaces |
pnpm lint |
ESLint with type-aware rules |
pnpm format |
Prettier write |
pnpm format:check |
Prettier check (no writes) |
pnpm test |
Run all workspace tests (vitest) |
pnpm db:migrate |
Apply migrations against the configured DB |
pnpm db:seed |
Seed the demo catalog (idempotent) |
Per-workspace shortcuts:
| Command | What it does |
|---|---|
pnpm --filter @shopping-app/api db:studio |
Open Prisma Studio (browse DB) |
pnpm --filter @shopping-app/api db:reset |
Drop, re-apply migrations, re-seed |
pnpm --filter @shopping-app/e2e install:browsers |
Download Playwright browsers (one-off) |
pnpm --filter @shopping-app/e2e test |
Run end-to-end browser tests |
| What | How |
|---|---|
| Unit + integration (API) | pnpm test |
| Typecheck | pnpm typecheck |
| Lint + format | pnpm lint && pnpm format:check |
| E2E in a real browser | pnpm --filter @shopping-app/e2e test (see e2e/README.md) |
| Load smoke | k6 run --env BASE_URL=http://localhost:3001 load-tests/smoke.js (see load-tests/README.md) |
| Dependency CVE audit | pnpm audit --audit-level high |
| Container vuln scan | runs on every PR in CI via Trivy |
End-to-end deploy needs an AWS account + a Route 53 hosted zone. Detailed
topology, costs and trade-offs are in
docs/aws-architecture.md.
One-time bootstrap (once per AWS account):
cd infra/terraform/bootstrap
terraform init
terraform apply -var "github_org=YOUR_ORG" -var "github_repo=shopping-app"Copy the printed github_role_arn into
.github/workflows/deploy.yml as AWS_ROLE_ARN.
Provision staging:
cd infra/terraform/envs/staging
cp terraform.tfvars.example terraform.tfvars # fill in zone id + hostnames
terraform init
terraform applySeed the app secrets (Terraform creates them empty so plaintext is never in state):
aws secretsmanager put-secret-value --secret-id shop-staging/jwt-secret \
--secret-string "$(openssl rand -base64 48)"
aws secretsmanager put-secret-value --secret-id shop-staging/stripe-secret-key \
--secret-string "sk_test_..."
# …repeat for the others (see launch-checklist.md)First deploy kicks off automatically when CI pushes to main. It builds
the Docker image, pushes to ECR, syncs the SPA to S3, invalidates CloudFront,
and updates the ECS service.
Promote to prod by tagging:
Scan report · 2026-09-14
- ✓ Prohibited terms or links
- ✓ Repository eligibility
- ✓ slopscore.md paperwork
- ✓ Content policy
- ✓ Risk review — +10 owner has 0 followers
0 comments
log in to comment.