gora.
Jusmila — motorcycle parts catalog in Lithuania

Tech stack

Turborepo monorepo, NestJS API, Next.js storefront, Vite admin, chrome-devtools MCP scraper.

~4 min read · 858 words

TL;DR

Turborepo monorepo with three apps: API (NestJS + TypeORM + PostgreSQL 16 + Redis + Socket.IO), public storefront (Next.js 14 + next-intl + TanStack Query), admin (Vite 6 + React 18 + Leaflet). Catalog scraping — chrome-devtools MCP instead of Puppeteer. Hosting — a dedicated ARM64 VPS.

Stack and architecture

Monorepo

Turborepo builds three apps and shared packages:

auto-parts/
├── apps/
│   ├── api/        NestJS — REST + WebSocket
│   ├── web/        Next.js — public storefront
│   └── admin/      Vite + React SPA — internal admin
├── packages/
│   ├── shared/     shared DTO types
│   └── ui/         shared React components (for admin)
├── scraper/        chrome-devtools MCP scraper
└── deploy/         docker-compose, nginx configs

Turborepo gives build caching (CI rebuilds only changed apps); turbo dev runs all three in parallel for local dev.

API (NestJS)

NestJS 10 with TypeORM:

  • Auth: @nestjs/jwt + @nestjs/passport + bcrypt. Cookie-based for admin, Bearer tokens for future integrations.
  • Storage: PostgreSQL 16 via TypeORM. Main entities — Product, Category, Fitment, Order, OrderItem, Customer, DeliveryZone, Inventory, Supplier.
  • Cache: Redis via @nestjs/cache-manager + @keyv/redis. Cached: filtered catalog, fitment tables, geo-bindings.
  • Real-time: @nestjs/platform-socket.io with @socket.io/redis-adapter — horizontal scaling, stock sync between admin and storefront without reload.
  • Docs: @nestjs/swagger generates the OpenAPI spec at /api/docs.
  • Defence: @nestjs/throttler for rate-limiting (especially on search endpoints), global guards for JWT and role checks.

Storefront (Next.js)

Next.js 14 with App Router. Key modules:

  • next-intl — multilingual (LT / RU / EN). Each language is its own URL prefix (/lt, /ru, /en), statically rendered for SEO.
  • TanStack Query (React Query) — server cache for catalog, cart, order statuses. Refetch on window focus, refresh via WebSocket invalidation.
  • Framer Motion — transition animations between states (card loading, add-to-cart).
  • Zustand — client state for cart and catalog filters.
  • Tailwind CSS v4 — styling, no separate design system; components are written in place.

The catalog renders in mixed mode: category lists — statically (ISR), product cards — server-side with invalidation by timer or admin event.

Admin (Vite SPA)

Vite 6 + React 18. A heavy SPA — many screens, frequent re-renders, so the Vite dev mode gives fast hot-reload without lag, unlike a Next.js dev build.

  • Leaflet — Lithuanian maps for delivery zones. Used without external APIs (OpenStreetMap tiles).
  • jsPDF + jspdf-autotable — export delivery notes and reports right in the browser, no server rendering.
  • react-quill-new — rich-text for product descriptions (HTML with image support).
  • rrweb-player — embedded player for session replay from client complaints.
  • Recharts — analytics charts (sales, conversion).
  • react-router-dom 7 — client routing; file-based not used.
  • Zustand — shared store, no Redux.

Auth — shared with the API via JWT + cookie on the admin-* subdomain (or basic-auth for very locked endpoints).

Scraper

A separate scraper/ folder with import scripts. The approach — chrome-devtools MCP, not Puppeteer or Playwright:

scraper/
├── README.md
├── scripts/
│   ├── import-mototex.ts
│   ├── import-anjese.ts
│   └── ...
└── data/         intermediate-result cache

MCP (Model Context Protocol) launches a real browser and drives it from a Node process. From the source site's perspective — this is an ordinary Chrome user, so it works on sites with aggressive anti-bot defences.

Import cycle: open the catalog list → walk pages → on each card pull SKU, prices, photos, fitment table → normalise → send to the API → API creates or updates the product. Progress flies into the admin over WebSocket, the operator sees the import in real time.

Hosting

  • ARM64 VPS. Dedicated server, user parts. ARM is chosen for cost and lower power draw — for a catalog without heavy compute it's non-critical.
  • nginx — reverse-proxy, TLS terminator, static. Virtual hosts for the public storefront and admin on different subdomains.
  • Docker compose — all services (api, web, admin, postgres, redis) in one file. Deploy — docker compose pull && docker compose up -d.
  • GitHub Actions — CI: lint, type-check, build → push image to GHCR. Smart path filter rebuilds only the changed apps.

Crypto payments

The API has bitcoinjs-lib and bip32 wired in — infrastructure for accepting crypto payments (BTC, ETH via derived keys). BIP32 address derivation: one master key per platform, derived addresses for each order — this gives incoming-transaction tracking without exposing the master key.

It's optional on the storefront for now, but the architecture is there; for the Russian-speaking segment in Lithuania it could become a settlement channel when euro acquiring isn't convenient.

What I'd rewrite

On a fresh iteration:

  • Price attestation. Today prices are imported as-is. A normalisation layer with history could be added: "this part used to cost Y at supplier X, now Z — 30% delta, isn't this counterfeit?"
  • CDN for photos. Images currently go through nginx from disk. As the catalog grows — worth switching to S3/R2 + CDN.
  • Full-text search. TypeORM LIKE handles 10,000 SKUs, but at 50,000+ you need Postgres FTS or Meilisearch.

For now the platform runs in production, orders flow, catalog import is automated. Further iterations depend on assortment and traffic growth.