Skip to content

Tech stack

What this answers: what's in the box right now, why each piece is there, where the soft spots are.

Versions are accurate as of feat/issue-34 (2026-04-28) — see package.json for the live state.

At a glance

Layer Choice Notes
Runtime Node 22 LTS No engines pin in package.json — see Gaps
Language TypeScript 5 strict mode on
Frontend Next.js 16.2.4 (App Router, RSC) + React 19.2.4 Both very recent — see Cross-cutting risks
Styling Tailwind 4 (@tailwindcss/postcss) No Prettier — Tailwind 4 has its own formatting expectations
UI primitives Radix react-slot (1 package) + lucide-react + motion + cva + clsx + tailwind-merge 7 of 8 listed @radix-ui/* packages currently unused — see Gaps
Backend / API Next.js route handlers + server actions Same process as the web app today
ORM Prisma 7.8 + @prisma/adapter-pg 7.8 + pg 8.20 Prisma 7 reads url from prisma.config.ts, not schema.prisma — see ADR-0001
Database Postgres 17-alpine (Docker) on host port 5433 ADR-0001 explains 5433
Cache / queue Redis 7-alpine (Docker) Container running, but bullmq + ioredis listed in deps and zero imports in src/ — queue infra is M2 (#18)
Scraper cheerio 1.2 + undici 7 + custom src/scrapers/{core,sites}/* Run via tsx scripts/run-scrapers.ts
ID generation nanoid 5 Click tokens
Validation zod listed but zero imports in src/ — see Gaps
Build (app) next build Standard
Build (docs) mkdocs-material via bash scripts/build-docs.sh (or mkdocs build --strict) Cloudflare Pages auto-deploys on push
Test Vitest 4.1 (+ @vitest/ui) — 7 spec files No E2E layer; no Playwright; no MSW
Lint ESLint 9 flat config (eslint-config-next 16.2.4) No Prettier configured
Format See Gaps
Observability None today — see Gaps
CI GitHub Actions: docs build only .github/workflows/docs.yml runs mkdocs build --strict on PRs that touch docs/. No app CIvitest, tsc, eslint are not gated in CI yet
Hosting docs only (Cloudflare Pages, live) App hosting unresolved — #19, see RFC-0001

Per-layer inventory

Runtime

Item Version Purpose Lockdown
Node.js 22 LTS (dev box) JS runtime for app, scrapers, scripts Floating — no engines field in package.json, no .nvmrc, no .node-version
npm + package-lock.json npm 10.x Package manager + lockfile Lockfile committed
TypeScript ^5 Strict-mode types across src/, tests/, scripts/, prisma/seed.ts Caret on a major — accepts any 5.x
tsx ^4.21.0 TypeScript execution for scrapers + Prisma seed Caret major
ts-node ^10.9.2 Listed in devDeps but not used in npm scripts — likely a Prisma 6 carry-over Caret major

Frontend

Item Version Purpose Lockdown
Next.js 16.2.4 App Router, RSC, route handlers, server actions, middleware Exact pin (no caret)
React 19.2.4 UI library Exact pin
React DOM 19.2.4 DOM renderer Exact pin
Tailwind CSS ^4 Utility CSS, design tokens via @theme inline in src/app/globals.css Caret major
@tailwindcss/postcss ^4 Postcss plugin used by next build Caret major
eslint-config-next 16.2.4 ESLint flat-config preset bundled with Next 16 Exact pin (matches Next pin)
Tailwind 4 — no Prettier Tailwind 4 enforces no class-sorting plugin; we have no Prettier at all

UI primitives

Item Version Purpose Lockdown
lucide-react ^1.11.0 Icon set — 13 imports across src/ Caret major
motion ^12.38.0 Framer Motion successor — used in 2 landing components for stagger reveals Caret major
class-variance-authority ^0.7.1 Variant API for Button + a few domain primitives — 3 imports Caret pre-1.0
clsx ^2.1.1 Conditional class names — 1 import (src/lib/cn.ts) Caret major
tailwind-merge ^3.5.0 De-dupe Tailwind classes inside cn() — 1 import Caret major
@radix-ui/react-slot ^1.2.4 Used by Button (cva pattern) — 1 import Caret major
@radix-ui/react-checkbox ^1.3.3 Unused — listed for upcoming filter sidebar Caret major
@radix-ui/react-dialog ^1.1.15 Unused — listed for choose-component sheet Caret major
@radix-ui/react-dropdown-menu ^2.1.16 Unused Caret major
@radix-ui/react-select ^2.2.6 Unused Caret major
@radix-ui/react-slider ^1.3.6 Unused — listed for price-range filter Caret major
@radix-ui/react-tabs ^1.1.13 Unused Caret major
@radix-ui/react-tooltip ^1.2.8 Unused Caret major
cmdk ^1.1.1 Unused — Cmd+K palette is wired into TopNav UI but the keystroke handler is not bound (called out in REBUILD-V1.md item 3) Caret major
recharts ^3.8.1 Unused — pre-installed for price history charts (REBUILD-V1.md item 4) Caret major
zustand ^5.0.12 Unused — explicitly rejected during the rebuild (URL-driven build state, see REBUILD-V1.md) Caret major

Backend / API

Item Version Purpose Lockdown
Next.js route handlers (Next 16) /api/go/r/[retailerId]/p/[listingId] — click tracking + 302 redirect
Next.js server actions (Next 16) Reserved for "save build" wiring (UI-only today, REBUILD-V1.md item 2)
undici ^7.25.0 Native fetch for scrapers — 1 import (src/scrapers/core/http.ts) Caret major
nanoid ^5.1.9 12-char URL-safe click tokens — 1 import Caret major
zod ^4.3.6 Unused — no validation layer wired yet Caret major

Database

Item Version Purpose Lockdown
Postgres 17-alpine Primary store. Models: Retailer, Product, Listing, ListingPrice, Click — see Prisma models Pinned via Docker tag
Host port 5433 Avoids conflict with the hummingbot Postgres on 5432 — ADR-0001
Prisma ^7.8.0 ORM, migrations, generated client Caret major
@prisma/adapter-pg ^7.8.0 Required at PrismaClient construction in Prisma 7 Caret major
pg ^8.20.0 Underlying pg driver for the adapter Caret major
Prisma seed tsx prisma/seed.ts Configured in prisma.config.ts migrations.seed, not package.json.prisma.seed (Prisma 7 quirk — see AGENTS.md)
Migrations prisma/migrations/ 1 migration today (initial schema)

Cache / queue

Item Version Purpose Lockdown
Redis 7-alpine Container running in docker-compose.yml for upcoming queue use. Persistence: default RDB Pinned via Docker tag
bullmq ^5.76.2 Unused in src/ today — pre-installed for #18; the runtime choice is itself an open question — see RFC-0002 Caret major
ioredis ^5.10.1 Unused — paired with bullmq Caret major

Scraper

Item Version Purpose Lockdown
cheerio ^1.2.0 jQuery-style HTML parsing — 1 import (src/scrapers/core/parse.ts) Caret major
undici ^7.25.0 HTTP fetch (see backend row) Caret major
Per-site parsers src/scrapers/sites/{pcandparts,souq961,macrotronics}.ts
Runner tsx scripts/run-scrapers.ts Manual today; cron / queue is M2 (#18)

Build / deploy

Item Version Purpose Lockdown
Next.js build next build (Next 16) App build
Next.js dev next dev Local dev on :3000
MkDocs Material (via docs/requirements.txt, pinned in CI) Docs site build
bash scripts/build-docs.sh Local docs build (does not pass --strict); CI uses --strict — see .github/workflows/docs.yml
Cloudflare Pages live Auto-deploys docs on push to master — see Runbook → Deploy docs site
App hosting Unresolved — #19, see RFC-0001

Test

Item Version Purpose Lockdown
vitest ^4.1.5 Test runner Caret major
@vitest/ui ^4.1.5 UI watch mode Caret major
Specs 7 files tests/{app,lib,rules,scrapers}/... — mirrors src/ per packages.md
E2E None — no Playwright, no Cypress, no MSW

Lint

Item Version Purpose Lockdown
eslint ^9 Lint runner, flat config in eslint.config.mjs Caret major
eslint-config-next 16.2.4 Bundled Next preset (core-web-vitals + typescript) Exact pin
Format No Prettier, no dprint, no Tailwind class-sorter

Observability

Nothing instrumented today. No log shipper, no APM, no error tracker (Sentry/Highlight/etc.), no metrics, no alerting. The architecture page calls out the intent (deployment.md § Observability) — implementation is deferred to whatever the #19 hosting decision pulls along.


Lockdown summary

Caret-floating is the default across the repo. Three packages are pinned to an exact version:

  • next: 16.2.4
  • react: 19.2.4, react-dom: 19.2.4
  • eslint-config-next: 16.2.4

The Next pin is intentional — Next 16 + React 19 are recent, and a minor bump can introduce regressions in App Router edge cases. The React exact pin pairs with the Next pin (Next 16 ships compiled against React 19.2.4). eslint-config-next exact pin matches the Next version. Everything else floats on caret. The lockfile (package-lock.json) is committed, so day-to-day builds are reproducible regardless of caret ranges.


Gaps

Spots where the stack and the docs/code diverge. Each gap is followed by a one-line "what to do."

Deps in package.json, zero imports in src/

Package Why it's there Status Recommendation
bullmq Pre-installed for #18 queue infra Intentional pre-install, but the runtime choice is itself open — see RFC-0002 Keep until RFC-0002 lands; if pg-boss wins, drop both bullmq and ioredis
ioredis Pairs with bullmq Same as above Same as above
cmdk Cmd+K command palette — UI hooks not bound Intentional pre-installREBUILD-V1.md item 3 Keep; wire up in M2 polish
recharts Price history charts Intentional pre-installREBUILD-V1.md item 4 Keep; wire up in M2 polish
zustand State store — explicitly rejected during the rebuild Dead — URL-driven state replaced it (REBUILD-V1.md) Drop in a tidy-deps PR
zod Runtime validation — never wired Probably intentional (server-action / API validation expected) Keep until M2 server actions land; if still unused at end of M2, drop
@radix-ui/react-checkbox Filter sidebar (M2 polish item 5 in REBUILD-V1.md) Intentional pre-install Keep
@radix-ui/react-dialog Choose-component sheet Intentional pre-install Keep
@radix-ui/react-dropdown-menu Reserved Intentional Keep or drop with the next radix audit
@radix-ui/react-select Reserved Intentional Keep or drop
@radix-ui/react-slider Price-range filter (M2 polish item 5) Intentional Keep
@radix-ui/react-tabs Reserved Intentional Keep or drop
@radix-ui/react-tooltip Reserved Intentional Keep or drop
ts-node devDep, not referenced by any script Likely a Prisma 6 carry-over (Prisma 7 uses tsx) Drop in a tidy-deps PR

The dead deps (zustand, ts-node) are minor — drop them when convenient. The "intentional pre-installs" should not be dropped without coordinating with the upcoming tickets that bring them in.

Used / present, undocumented

  • Tailwind 4 + no Prettier — Tailwind 4 has known opinions on class formatting. Without Prettier or a class-sorter, formatting is by manual convention. Worth a one-line decision on whether to add Prettier + prettier-plugin-tailwindcss or stay manual.
  • No engines field, no .nvmrc — the dev box is on Node 22, but a contributor on Node 18 or 20 would silently get a different runtime. Add "engines": { "node": ">=22" } in package.json and a .nvmrc (one line: 22).
  • No app CI gate.github/workflows/docs.yml is the only workflow. vitest, tsc --noEmit, eslint are run locally only. A red test on master would not surface until someone runs the suite. Worth adding a ci.yml workflow that runs the three.
  • Observability: nothing — see the layer note above.

Pinned weirdly

  • react: 19.2.4 and react-dom: 19.2.4 are exact-pinned without a caret. This is intentional given the Next 16 pin, but the relationship is implicit. Worth a one-line comment in package.json (or, more durably, an ADR after #19 lands).
  • eslint-config-next: 16.2.4 exact pin is correct (must match Next major.minor) but is easy to forget when the Next pin moves. Consider a comment-style note in eslint.config.mjs.

Cross-cutting risks

Freshness band

Five major dependencies in the stack shipped within the last ~6 months:

  • Next.js 16 (released late 2025)
  • React 19 (early 2025 stable; Next 16 bundles React 19.2)
  • Prisma 7 (late 2025; the prisma.config.ts + @prisma/adapter-pg shape is the Prisma 7 model)
  • Tailwind 4 (late 2025)
  • Vitest 4 (early 2026)

Each of these alone is fine. The cluster is the risk: a single supply-chain incident or a regression in one major patch would cascade — and ESLint's eslint-config-next is exact-pinned to the Next version, so you can't easily jump only one of them. Lockfile + careful upgrade discipline mitigates day-to-day; a quarterly "bump the bleeding-edge stack" ADR would formalise the cadence.

Mitigation already in place: package-lock.json committed; CI does not auto-bump.

Supply chain

  • motion is the rebrand of Framer Motion as an independent library; ownership is stable but the rename is recent.
  • cheerio 1.2 is mature.
  • undici 7 is the Node-team-maintained HTTP client used internally by Node's fetch.
  • All other deps are mainstream.

No deps flagged as unmaintained, deprecated, or with known security advisories at the time of this audit. npm audit is not run in CI — adding it (or pnpm audit --prod) to a future ci.yml is a low-cost win.

EOL dates

Component Support window
Node 22 LTS active until April 2027 (maintenance until Apr 2028)
Postgres 17 Major-version support until November 2029
Redis 7 Maintenance until at least 2027
Tailwind 4 Active major
React 19 Active major
Next.js 16 Active major; Next typically supports the previous one major back
Prisma 7 Active major

No piece of the stack is approaching EOL. The first one to retire will be Node 22 — by then the queue/search/hosting decisions will have been made and most of the framework majors will have moved.

Security

  • No secrets in the repo; .env is gitignored, .env.example is the committed template.
  • IP_HASH_SECRET defaults to a literal string dev-secret-rotate-in-prod in src/app/api/go/... (env-vars.md § IP_HASH_SECRET) — must be rotated to a real ≥32-byte random value before any prod deploy.
  • No CSP, no rate limiting, no auth — all M2 concerns (#11, #12).
  • next.config.ts images.remotePatterns is the only outbound-image allow-list — fine for now; revisit once images move to a CDN.

What to read after this