Tech Stack
The truth lives in package.json and the config files at
the repo root. This page is the human-readable map of it. Versions below are the
semver ranges as declared (^x.y.z) — run npm ls <pkg> for the exact resolved
version.
🧱 Layers at a glance
| Layer | What we use | Version |
|---|---|---|
| Framework | Next.js — App Router, React Server Components, route handlers under app/api/** | ^15.5.6 |
| UI runtime | React + React DOM | ^19.2.3 |
| Styling | TailwindCSS v4 (stable) via @tailwindcss/postcss; @tailwindcss/typography; tailwind-merge, tailwindcss-animate, class-variance-authority, clsx | ^4.1.18 |
| Components | Radix UI primitives + radix-ui + @base-ui-components/react (shadcn/ui pattern); cmdk command palette; @dnd-kit/* drag-and-drop | radix-ui ^1.4.3 |
| Icons | lucide-react, @heroicons/react, @remixicon/react, react-icons | lucide-react ^0.479.0 |
| Toasts / theming / motion | sonner, next-themes, motion | sonner ^2.0.3 |
| Language | TypeScript — strict: true, moduleResolution: bundler, @/* path alias → src/* | ^5.9.3 |
| Data | PostgreSQL (self-managed; Neon planned) via the pg driver + Drizzle ORM; migrations by drizzle-kit; drizzle-seed + @faker-js/faker for seeds | drizzle-orm ^0.45.1 / drizzle-kit ^0.31.9 / pg ^8.14.0 |
| Auth | Clerk — @clerk/nextjs (app), @clerk/testing (e2e); custom role-based access (student/admin) | ^6.36.6 |
| AI | Anthropic SDK — fire-and-forget issue-intake triage (src/lib/ai/) | ^0.92.0 |
| Object storage | DigitalOcean Spaces (S3-compatible) via @aws-sdk/client-s3 + @aws-sdk/s3-request-presigner | ^3.975.0 |
| Webhooks | svix — verifies Clerk webhook signatures (src/app/api/webhooks/route.ts) | ^1.64.1 |
| Observability | Sentry — @sentry/nextjs (build-time wiring is opt-in via SENTRY_DSN); pino + pino-pretty structured logging (src/lib/logger.ts) | @sentry/nextjs ^9.40.0 / pino ^9.6.0 |
| i18n | next-intl — [locale] segment + src/i18n/request.ts plugin | ^4.7.0 |
| Server state / tables | TanStack react-query (+ react-query-devtools), react-table, react-virtual | react-query ^5.90.16 |
| Charts / dates / parsing | recharts; date-fns + dayjs + react-day-picker; csv-parse, gray-matter, remark/remark-gfm/remark-html, nanoid/uuid, zod | recharts ^2.15.4 / zod ^4.2.1 |
| Tests | Vitest (^4.1.7) for unit logic + Playwright (@playwright/test ^1.59.1) for e2e | — |
⚠️ Tailwind v4 is stable, not alpha. Earlier docs flagged it as a bleeding-edge risk — that was true in early 2025 and is no longer the case. It is wired through
@tailwindcss/postcssin production today.
🛠️ Scripts
All scripts run via npm run <name> (see package.json).
TypeScript scripts execute through tsx.
Dev / build
| Script | Command | Notes |
|---|---|---|
dev | next dev --turbopack | Local dev server on Turbopack |
build | next build | Production build |
start | next start | Serve the production build |
Database — generate / migrate / seed
| Script | Command | Notes |
|---|---|---|
generate | drizzle-kit generate | Generate a migration from src/db/schema.ts |
migrate | drizzle-kit migrate | Apply pending migrations (also runs on container start) |
seed:enums | tsx src/db/seed-enums.ts | Enum/lookup tables |
seed:users | tsx src/db/seed-users.ts | Users |
seed:q | tsx src/db/seed-questions.ts | Questions |
seed:cml | tsx src/db/seed-course-modules-lessons.ts | Courses / modules / lessons |
seed:events-teams | tsx src/db/seed-events-teams.ts | Events + teams |
seed:institutions | tsx src/db/seed-institutions.ts | Institutions |
seed:tc-bootstrap | tsx src/db/seed-test-cycles-bootstrap.ts | Test-cycles bootstrap data |
seed:tc-onboarding | tsx src/db/seed-hackorda-qa-cycle.ts | The "Hackorda Onboarding — QA shake-down" cycle (needs an ADMIN user) |
seed:api-key | tsx src/db/seed-api-key.ts | API key |
Test
| Script | Command | Notes |
|---|---|---|
test | vitest run | Unit tests (src/**/*.test.ts) — CI check job |
test:watch | vitest | Watch mode |
test:coverage | vitest run --coverage | v8 coverage over src/lib/** |
test:e2e | playwright test | Browser e2e (tests/e2e/**); gated on E2E_ENABLED |
test:e2e:ui | playwright test --ui | Playwright UI mode |
test:e2e:debug | playwright test --debug | Single test, inspector attached |
test:e2e:install | playwright install chromium webkit | Install browser engines |
Audit / lint
| Script | Command | Notes |
|---|---|---|
lint | eslint . | Flat config (eslint.config.mjs), next/core-web-vitals + next/typescript |
audit | tsx scripts/audit-frontend.ts | Human-readable file-size report |
audit:check | tsx scripts/audit-frontend.ts --check | CI mode — exits 1 on regressions |
audit:write-baseline | tsx scripts/audit-frontend.ts --write-baseline-json | Lock in reductions |
🚀 Hosting
Not Vercel. The app is self-hosted on a DigitalOcean droplet running Docker. CI builds the image, pushes it to GHCR, then SSHes to the droplet to pull and recreate the container. Caddy terminates TLS and reverse-proxies to the app. PostgreSQL is self-managed on the same droplet (a move to managed Postgres / Neon is planned). The self-hosted GitHub Actions runner also lives on that box.
Migrations and seeds run automatically on container start
(scripts/docker-entrypoint.sh), so a fresh deploy is "push to main and wait."
See Deployment for the pipeline and host-resource runbook, and Database for connection topology, SSL, backups, and the managed-Postgres cutover plan.
🧰 Tooling config
| Concern | File | Highlights |
|---|---|---|
| Next.js | next.config.ts | next-intl plugin, opt-in Sentry wrap, serverExternalPackages: ['pg', …], DO Spaces image hosts |
| TypeScript | tsconfig.json | strict, moduleResolution: bundler, @/* → src/* |
| ESLint | eslint.config.mjs | Flat config; ignores docs-site/ |
| Drizzle | drizzle.config.ts | postgresql dialect, schema src/db/schema.ts, out src/db/migrations |
| Vitest | vitest.config.ts | Node env, src/**/*.test.ts, v8 coverage over src/lib/** |
| Playwright | playwright.config.ts | desktop-chromium + mobile-chrome + mobile-safari; auto-starts npm run dev |
✅ Code-quality gates
- File-size audit —
scripts/audit-frontend.tsenforces per-layer line caps (routes 600, components 400, hooks 500, lib 600, db 600). A baseline (scripts/audit-frontend-baseline.json) grandfathers existing offenders; CI fails when a new file exceeds its cap or a baselined file grows. Runnpm run auditbefore opening a PR. - Service-layer migration — routes are moving off direct
@/dbaccess to a 3-tierroute → lib/<domain>/<action> → @/dblayout. See Service layer for the canonical pattern.
🔄 Data flow
Client → TanStack Query hook → app/api route handler
→ lib/<domain> service (validation, transactions, biz logic)
→ Drizzle ORM → PostgreSQL
→ type-safe response back to the client