Stack
Concrete technology choices for the Alhuda Travel ERP. Cross-reference package.json for exact versions; the table below captures the load-bearing dependencies and the role each one plays.
Runtime surface
| Concern | Choice | Why |
|---|---|---|
| UI framework | React 18 (react, react-dom) |
Standard for the team; Suspense + concurrent features used for lazy route loading in src/App.tsx. |
| Build / dev server | Vite 5 with @vitejs/plugin-react-swc |
Fast HMR, ESBuild-style transforms, PWA via vite-plugin-pwa, gzip + brotli compression plugins. See vite.config.ts. |
| Language | TypeScript 5.8 (strict compile via npx tsc --noEmit) |
Enforced in CI; tsc --noEmit must be clean before PR review (CLAUDE.md §Testing). |
| Styling | Tailwind CSS 3.4 + tailwind-merge, tailwindcss-animate, class-variance-authority, clsx |
Utility-first; cva used inside src/components/ui/* shadcn-style primitives. |
| Headless UI primitives | Radix UI (@radix-ui/react-*, ~28 packages) |
Accessibility-first unstyled primitives, wrapped by the project's components/ui layer. |
| Icons | lucide-react |
Consistent icon set used across the app. |
| Theming | next-themes |
Light/dark toggle wired in src/components/theme-provider.tsx. |
| Routing | React Router 6 (react-router-dom) |
See src/App.tsx for the full route table and Frontend routing. |
| Data fetching / cache | TanStack React Query 5 | Default staleTime 5m, gcTime 10m, retry: 1, refetchOnWindowFocus: false — tuned for an internal ERP where data doesn't change under the user's nose (src/App.tsx:84-98). |
| Virtualisation | @tanstack/react-virtual |
Long list perf (journal lines, booking passengers, etc.). |
| Forms | React Hook Form 7 + @hookform/resolvers |
Paired with Zod for schema-driven validation. |
| Validation | Zod 3 | Shared type-safe validation between UI and API parameter parsing. |
| Backend SDK | @supabase/supabase-js 2 |
Used everywhere via src/integrations/supabase/client.ts. No custom Node API server. |
| Date handling | date-fns 3 |
No Moment.js; locale formatting done per-call. src/lib/date.ts wraps getDateStamp helpers. |
| Charts | recharts |
Dashboard, Finance reports. |
| Excel I/O | xlsx + xlsx-js-style — lazy-loaded |
Bundle-budget discipline — xlsx is only pulled in by the few screens that import/export spreadsheets; see scripts/check-bundle-budgets.mjs. |
| Toasts / notifications | sonner + Radix Toast (@radix-ui/react-toast) |
Two notification surfaces for different use cases. |
| Command palette | cmdk |
Quick actions. |
| Carousel / input masks | embla-carousel-react, input-otp, react-day-picker, vaul, react-resizable-panels |
Component-specific primitives. |
| Observability | Sentry (@sentry/react) |
Initialised in src/lib/observability.ts; error boundary in src/components/common/ErrorBoundary.tsx. |
| Web vitals | web-vitals |
Performance telemetry streamed through the observability layer. |
PWA and service worker
vite-plugin-pwa is configured with strategies: 'injectManifest' and points at src/sw.ts — we hand-write the service worker logic rather than relying on auto-generated Workbox config. Update prompts surface via @/components/common/PWAUpdateNotifier (src/App.tsx:13). See vite.config.ts:43 onward.
Test stack
| Concern | Choice | Where |
|---|---|---|
| Unit / integration | Vitest 2 + happy-dom |
vitest.config.ts, 570+ tests required to pass before commit (CLAUDE.md §Testing). |
| React tests | @testing-library/react + @testing-library/jest-dom + @testing-library/user-event |
Co-located *.test.tsx files. |
| Mock service worker | msw |
HTTP mocking where Supabase can't be spun up in-process. |
| E2E | Playwright (@playwright/test) |
playwright.config.ts, scripts in e2e/. |
| Coverage | @vitest/coverage-v8 |
npm run test:coverage. |
Build and release tooling
| Concern | Choice | Notes |
|---|---|---|
| Linter | ESLint 9 with eslint-plugin-react-hooks, eslint-plugin-react-refresh, typescript-eslint |
eslint.config.js. |
| Bundle analysis | rollup-plugin-visualizer |
ANALYZE=true npm run build emits dist/bundle-stats.html. |
| Bundle budgets | scripts/check-bundle-budgets.mjs |
Hard ceiling on per-chunk sizes; fails the build on regression. |
| Compression | vite-plugin-compression (gzip + brotli) |
Built assets are pre-compressed for Cloudflare Pages. |
| Release automation | semantic-release + @semantic-release/changelog + @semantic-release/git |
Conventional commits drive CHANGELOG.md and version bumps. |
| Supabase CLI | supabase (dev dependency) |
Local DB + Edge Functions workflow. |
Libraries that deliberately don't appear
- No Redux / Zustand — server state lives in React Query, local state in
useState. - No Axios — everything goes through
supabase-jsor a thinapiFetchwrapper over it. - No Moment.js —
date-fnsonly. - No Lodash — small targeted helpers live in
src/lib/*.
Why React Query + Supabase, not Redux
React Query's staleness model maps cleanly onto "data lives in Postgres and is fetched on demand". Adding Redux would mean two caches (client-owned and server-owned) and a whole class of reconciliation bugs; the team has chosen to skip it.
Further reading
package.json— authoritative dep list.vite.config.ts— build configuration.CLAUDE.md— test/build/PR rules.