Build better software with AI — by starting from requirements, not frameworks.
HAIH Site is a public demonstration of requirement-driven development. Instead of adopting tools first and fitting problems to them, we begin with real needs and choose solutions that earn their place.
Most AI-assisted development starts with a technology stack and hopes for the best. HAIH flips this: every solution must justify itself through purpose, capabilities, requirements, and evidence.
The website you see is its own proof. Every architectural decision, every dependency, every line of code exists because a real need demanded it — and that need is documented.
- Transparent decision-making — See exactly why each technology was chosen
- Working demonstrations — Not slides, not promises — running code you can verify
- Current architecture — React 19, TypeScript, Vite 8, Docker, Traefik, Varnish
- Modern delivery — SPA navigation, build-time rendering, code splitting, HTTP caching
| Layer | Technology | Purpose |
|---|---|---|
| UI | React 19 + TypeScript | Type-safe components with modern hooks |
| Routing | React Router 8 | SPA navigation, code splitting, SSR-ready |
| Build | Vite 8 | Fast HMR, optimized production bundles |
| Server | Node.js 22 + Express + sirv | Static files, SSR and GraphQL |
| Cache | Varnish 7 | Edge caching with explicit policies |
| Proxy | Traefik 3 | TLS termination, routing, load balancing |
| Container | Docker Compose | Reproducible environments |
Use Node.js 22.22.3 (the Docker image is pinned to this version).
# Development (with hot reload)
npm ci
npm run dev
# → http://127.0.0.1:3000
# Production build
npm run build
npm start
# → http://127.0.0.1:3000cd docker
# Development: app, cache proxy and TLS entry point
docker compose -f compose.yaml -f compose.dev.yaml up -d --build app varnish traefik
# → http://127.0.0.1:8080 (Traefik HTTP)
# → https://haih.localhost:8443 (Traefik HTTPS with HMR)
# → http://127.0.0.1:8088 (Traefik Dashboard)
# → http://127.0.0.1:3001 (App direct, also supports HMR)
# Production
docker compose -f compose.yaml -f compose.prod.yaml up -d
# → http://127.0.0.1:80 (Traefik → Varnish → Node.js)
# → https://127.0.0.1:443| Mode | Service | Default Port | Variable |
|---|---|---|---|
| Local | Vite/Node.js | 3000 | PORT |
| Docker dev | Traefik HTTP | 8080 | SITE_PORT |
| Docker dev | Traefik HTTPS | 8443 | SITE_PORT_HTTPS |
| Docker dev | Traefik Dashboard | 8088 | TRAEFIK_DASHBOARD_PORT |
| Docker dev | App direct | 3001 | APP_PORT |
| Docker prod | Traefik HTTP | 80 | SITE_PORT |
| Docker prod | Traefik HTTPS | 443 | SITE_PORT_HTTPS |
Set NETWORK_NAME in docker/.env to an existing external Docker network. Compose reads that file when run from docker/; the app also receives it through env_file. The full stack requires the certificates and monitoring configuration/secrets described in the monitoring runbook.
Before starting Traefik, place a certificate and key at docker/traefik/certs/localhost.crt and localhost.key. These files are ignored by Git. For a local self-signed certificate, run from the repository root:
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout docker/traefik/certs/localhost.key \
-out docker/traefik/certs/localhost.crt -days 365 \
-subj '/CN=haih.localhost' \
-addext 'subjectAltName=DNS:haih.localhost,DNS:localhost,IP:127.0.0.1'
chmod 600 docker/traefik/certs/localhost.keyAccept the local certificate in your browser, or use a certificate issued by a locally trusted development CA. haih.localhost must resolve to loopback on the browser's machine.
The Node HTTP server listens on PORT (3000 by default). Vite runs in middleware mode and attaches its WebSocket server to that same listener at /__vite_hmr. The browser derives the WebSocket host, port and protocol from the page URL: HTTPS uses wss://, HTTP uses ws://. No separate HMR port or hardcoded domain is needed.
APP_PORT changes only Docker's host HTTP port; keep the container's PORT=3000 because the port mapping and proxy upstream use it. SITE_PORT and SITE_PORT_HTTPS set the external proxy ports. For development without Docker, PORT=3002 npm run dev serves both HTTP and HMR on port 3002.
The development Compose override selects traefik/dynamic/development/ and varnish/development.vcl. Traefik terminates TLS and sends /__vite_hmr directly to the app. Other requests go through Varnish in pass mode, with X-Cache: PASS and Cache-Control: no-store, so development output is not retained. The production VCL and routes remain separate. To experiment with development caching, explicitly select a different VCL and account for stale modules and HTML.
React edits use HMR where supported. Server edits restart Node through tsx watch and can require a refresh. Restart the app after changing port configuration. Published development ports bind to loopback; remote access requires a tunnel or an explicitly configured proxy.
Verified on 2026-10-06 in Chromium against https://haih.localhost:8443: TLS HTML delivery through Traefik and Varnish, a successful wss://haih.localhost:8443/__vite_hmr connection, and a temporary React edit appearing without a document reload. Restoring the source and refreshing also returned current content. The browser check accepted the self-signed certificate; it does not establish OS/browser certificate trust.
Request Flow (Production):
Browser → Traefik → Varnish → Node.js → Static Assets / React Router SSR / API
Request Flow (Docker Development):
Browser → Traefik (TLS) → Varnish (pass) → Node.js + Vite Middleware
Browser → Traefik (WSS /__vite_hmr) → Node.js + Vite WebSocket
Public routes listed in react-router.config.ts are prerendered during the build. The production Node server serves generated files with sirv and forwards remaining requests to React Router's server build. The current Varnish policy assigns seven days to matching asset extensions and one hour to other successful responses, while /api bypasses caching. Publication still needs an explicit cache refresh policy; these settings alone do not establish production readiness.
app/
pages/ # Page views, sections, data and colocated assets
routes/ # Route integration and metadata
components/Layout/ # Shared Header, main content, Footer and basic responsive CSS
root.tsx # HTML document, shared layout integration, error boundary
server/
index.ts # HTTP server: static delivery, React Router SSR and API (compiled to build/node/)
docker/
compose.yaml # Base Docker Compose configuration
compose.dev.yaml # Development overrides
compose.prod.yaml # Production overrides
traefik/ # Traefik configuration, certs, logs
traefik.yml # Static config (entrypoints, providers)
dynamic/local/ # Default routing rules
dynamic/development/ # Development TLS and HMR routing
varnish/
default.vcl # Production cache policy
development.vcl # Development pass mode
npm run dev # Start Vite dev server
npm run types # TypeScript validation
npm run build # Production build
npm run test # All unit tests (Vitest)Every solution in this project follows a consistent evaluation model:
- Purpose — What need does it satisfy?
- Capabilities — What does it provide?
- Requirements — What does it demand?
- Trade-offs — Where is the cost justified?
- Evidence — What has been verified?
This isn't methodology theater. It's how we actually build — and how AI assistants can make better recommendations when they understand why, not just what.
The site includes an illustrated homepage, Solutions and a blog. The current integration combines prerendering, hydration, SPA navigation, route bundles, request-time SSR and a GraphQL API. See the test commands below for reproducible checks; production cache behavior and development HMR require checks against their actual endpoints.
See repository for license details.
haih.site — Requirement-driven development, demonstrated.
app/components/Layout provides the common Header, main landmark and Footer. The root document wraps route content and error fallbacks with this shell, so navigation stays available on 404 and route-error pages. Internal links use React Router, with active navigation and heading focus after forward navigation. A keyboard skip link targets the main content. The footer follows long content and sits at the bottom of short pages.
The shell uses mobile-first Linaria styled components from colocated styles.ts modules. Type checking and production builds check different aspects of this integration; browser checks are needed to establish hydration, hot updates and navigation behavior.
The monitoring runbook describes the production-artifact preview, Grafana dashboard, internal per-site probes, request/application logs and optional email/Telegram alerts. The installation is shared by sites behind one Traefik and uses an explicit site registry. Verification results distinguish the local checks from unverified production behavior. Request filtering is deferred.
| Command | Scope and prerequisites |
|---|---|
npm test |
All unit tests; no build, Docker or running server required. |
npm run test:watch |
Unit tests in watch mode. |
npm run test:coverage |
Unit coverage in the terminal and coverage/. |
npm run test:integration |
Builds the site, then checks generated SEO/SSR output, monitoring configuration generation and GraphQL metrics with isolated local listeners. |
npm run test:integration:watch |
Watches integration tests; run npm run build first and rebuild after changing page/build inputs. |
npm run test:integration:stack |
HTTP/cache and monitoring checks against an already running production preview stack. |
npm run e2e |
Playwright: Chromium, WebKit and mobile Chromium; builds and starts the production Node server automatically. |
npm run e2e:webkit |
Only the WebKit browser project. |
npm run e2e:report |
Opens the last Playwright HTML report. |
Install browser binaries once with npx playwright install chromium webkit (Linux hosts may also need npx playwright install-deps chromium webkit). Playwright owns port 4317 and fails if it is already occupied. To test an existing server or the real proxy/cache path, supply PLAYWRIGHT_BASE_URL; in that mode it does not build or start a server:
PLAYWRIGHT_BASE_URL=http://haih.localhost npm run e2e
TEST_URL=http://haih.localhost MONITORING_TEST_URL=http://127.0.0.1:18080 GRAFANA_TEST_URL=http://127.0.0.1:13000 npm run test:integration:stackStack tests require the monitoring preview, its provisioned Grafana password file, and a TEST_URL routed through Traefik and Varnish. They are kept separate from local integration tests because they require external infrastructure. The suite fails when that infrastructure is unavailable; it does not silently skip checks. Failure injection and notification fixtures remain explicit test:monitoring:failure and test:monitoring:notifications commands and are never part of automatic test discovery.
Add unit tests under tests/unit/, or colocate *.test.ts / *.test.tsx in app/ (*.test.ts in server/). Put local integration tests in tests/integration/, stack checks in tests/stack/, and browser *.spec.ts files in tests/e2e/. Each runner discovers its own suite. Unit tests currently use the Node environment; browser behavior belongs in Playwright. Configurations use CommonJS exports to avoid adding default exports. Coverage measures the configured SEO and server source scope; it is unit coverage, not combined integration/browser coverage.
Playwright checks hydration errors, same-document navigation, heading focus, metadata updates, browser history, deep-link refresh and 404 responses. Its default local server run does not prove Traefik/Varnish cache behavior or development HMR.