An exhaustive technical description of the Deal ex Machina website: stack, architecture, code quality, CI/CD, security, privacy, GDPR, EU AI Act, performance scores, roadmap, and why the web is the demo.
This site is the demo. Not a side project with a separate portfolio: the website you are reading is the proof of technical depth, from infrastructure and security to front-end performance and developer experience. It was fully hand-coded with Cursor [1] (AI-assisted IDE), with no low-code or page builders. What follows is a CTO-level, bottom-to-top walkthrough of how it is put together: runtimes, data, APIs, quality gates, CI/CD, security, and the performance numbers we hold ourselves to.
Why not a classic showcase site (WordPress, Wix, Squarespace)? Because for a technical consultancy, the site is the product: every dependency, every API, every header and cookie is a statement. A showcase site built with themes and plugins hides the very thing we sell—architecture, security, performance—behind a black box. Here, there is no theme to blame, no plugin to patch: one codebase, full control, type-safe from DB to UI, and a deployment that we can explain line by line. The narrative is simple: what you see is what we build.
Node.js [2]: Pinned to >=20.19.6 (and npm >=10.8.2) via package.json engines. We use the LTS line and avoid floating majors in production.
TypeScript [3]: 5.9.x with strict mode (strict: true, noEmit: true, isolatedModules: true, moduleResolution: "bundler"). No any in production code; path aliases @/* for clean imports. The codebase is ESM-only where possible (see project conventions).
Framework: Next.js 16 [4] (App Router). We use the standalone output for the Docker/Koyeb deployment and support static export for Cloudflare Pages when NEXT_OUTPUT=export or CLOUDFLARE_PAGES=1. So: one codebase, two deployment targets (Node server vs static + edge).
Database: PostgreSQL [5] accessed via Drizzle ORM [6] (drizzle-orm, drizzle-kit). Schema and migrations live in drizzle/; we use db:push, db:generate, db:migrate, and db:studio for development. Connection uses pooling (DATABASE_POOLING_URL_IP4 or equivalent); all writes go through a shared transaction helper (withTransaction) with rollback on failure and structured logging.
Auth and storage: Supabase [7] (SSR-compatible client and server utilities) for 6-digit email OTP sign-in and any Supabase-backed features. Sessions rely on Supabase cookies (HttpOnly, Secure, SameSite per your project settings).
Content: Content Collections [8] (@content-collections/core, @content-collections/markdown, @content-collections/next) for the blog. Markdown lives in content/blog/ with a Zod [9] schema (title/en/fr, excerpt, slug, category, accessLevel, etc.). We use remark-gfm [10] for GitHub Flavored Markdown. Build-time compilation only; no runtime markdown parsing on the client.
API routes (Next.js App Router):
POST /api/chat — streaming chat with the AI (Wagmi); session limits, rate limiting, and content moderation. Contact with the team is through the chat only — no separate contact form.GET/POST /api/chat/status — chat status/availability.GET /api/llm/status — LLM provider status.GET /api/health — health check; minimal payload in production (status only), richer in development (uptime, memory, env).POST /api/contacts/classification/request — contact classification (used by Wagmi for role-based behaviour).GET /api/auth/callback — Supabase auth callback.AI/LLM: Vercel AI SDK [11] (ai, @ai-sdk/openai, @ai-sdk/openai-compatible, @ai-sdk/react) with Assistant UI [12] (and react-ai-sdk, react-markdown) for the chat UI. LLM config is validated with Zod (LLM_API_URL, LLM_MODEL, LLM_INTERFACE, LLM_API_KEY); production requires LLM_API_URL. We support OpenAI-compatible endpoints (e.g. local Ollama).
Dual-tier LLM architecture: The chat operates two model tiers. Anonymous visitors are served by a small model (Qwen 2.5 1.5B on CPU via Koyeb) — fast, cheap, but limited in reasoning. Authenticated users unlock a larger GPU-backed model with deeper context handling. Fallback routing (GPU → CPU) ensures availability even when the GPU backend is down. Model selection is transparent: visitors see a notice that Wagmi is in "small model mode" and are nudged to authenticate.
Local RAG for small-model grounding: Because a 1.5B model hallucinates easily, we built a lightweight BM25-style RAG (local-rag.ts). At query time, the user message is tokenized (with bilingual EN/FR stopword removal), scored against pre-segmented chunks from wagmi-skills.md and ai.txt, and the top 4 matching snippets are injected into the system prompt. No vector database, no embeddings — just token overlap scoring. This is enough to ground answers on verified company facts and prevent the small model from inventing services, people, or partners that don't exist.
SFT dataset for fine-tuning: We generate a Supervised Fine-Tuning dataset (scripts/generate-wagmi-sft-dataset.ts) from the blog posts, the knowledge base, and ai.txt. The script produces 267 training examples and 47 eval examples in JSONL format, covering identity guardrails, service descriptions, authentication nudging, uncertainty expression, and conciseness requirements — all in both French and English. The dataset is designed for frameworks like Unsloth and targets the Qwen 1.5B model specifically.
Behavioural benchmark: A dedicated benchmark script (scripts/benchmark-rag-qwen15b.ts) runs 20+ test cases against the small model with and without RAG context, measuring factual accuracy, hallucination rate, auth-upsell compliance, and latency. This is the quality gate for the small model: if it fails the benchmark, it doesn't ship.
MaxTokens capping: Small LLMs (1.5B–3B) often fail to emit an end-of-sequence token, producing correct content followed by infinite repetition. We cap maxTokens at 300 for small models (~200 words, matching the prompt guidelines) and 1024 for larger models.
Validation: Zod everywhere — request bodies, env vars, and content-collections schema. Invalid input fails fast with typed error responses.
Errors: Custom ApiError hierarchy (api-error.ts): ValidationError, NotFoundError, etc., with toJSON() for consistent API responses and integration with a structured logger (request id, session id, module). No raw stack traces to the client in production.
UI: React 19 [13] with Tailwind CSS [14] and Radix UI [15] primitives (Avatar, Dialog, Label, Slot, Tooltip). We use class-variance-authority and tailwind-merge for component variants. Icons: lucide-react (tree-shaken via Next.js optimizePackageImports). Zustand for client state where needed.
i18n: next-intl [16] (v4) for EN/FR: messages in src/i18n/messages/{en,fr}.json, locale in the path ([locale]), and shared metadata/alternates for SEO.
Routing: App Router with [locale] and route groups: (routes) for blog and pages, (legal) for legal pages. Canonical URLs and hreflang are generated in metadata (see metadata/utils.ts) so every page has correct <link rel="canonical"> and alternates.
Performance (front-end):
dynamic(..., { ssr: false }) so the main bundle stays small; webpack splitChunks separate framework, lib (Radix, assistant-ui, lucide), vendor, and common.deviceSizes/imageSizes tuned, CSP for images; for static export we use unoptimized where required.adjustFontFallback to avoid CLS; display: swap and preload where appropriate.SEO and crawlers: robots.txt, llms.txt, and ai.txt for AI crawlers; Schema.org JSON-LD generated server-side; sitemaps for the site and blog. Auth/error pages are noindex.
Linting and formatting: Biome [18] (v2). We use biome check and biome format; config enforces double quotes, 120 line width, LF, and organized imports. No ESLint/Prettier in this project.
Git hooks: simple-git-hooks + lint-staged. On pre-commit we run Biome on *.{js,jsx,ts,tsx} and actionlint on .github/workflows/*.{yml,yaml}. So no unformatted or lint-break commits.
Tests:
src/__tests__/unit, src/__tests__/integration, src/__tests__/security, and src/components/__tests__. Coverage with @vitest/coverage-v8; we run test:run in CI and test:ci with coverage.e2e/ (chat, hydration, error boundaries, contacts recognition). No flaky patterns; tests are part of the definition of done.@lhci/cli): lighthouserc.js defines assertions (FCP, LCP, TBT, CLS, Speed Index, accessibility, best practices, SEO). Runs on PRs to main/dev and on demand.Type checking: tsc --noEmit as a separate step (type-check). CI runs it so type safety is enforced before merge.
Dependencies: Dependabot is enabled (npm weekly, GitHub Actions [22] monthly) with grouped minor/patch updates. We run npm audit --audit-level=critical in CI before build. Overrides for known issues (glob, rimraf, tar, cross-spawn) are declared in package.json.
GitHub Actions:
dev (and workflow_dispatch): install deps, critical audit, lint, test:run, then Docker [24] build and push to Docker Hub (jeanbapt/deal-ex-machina-web). Second job: Koyeb CLI to update the deal-ex-machina-staging/web service (Docker image, env, health check on /api/health, 60s grace period). We wait for deployment HEALTHY and then hit the staging URL for a final health check.npm ci, build (with placeholder env), then npm run lighthouse; results uploaded as artifacts (and optionally to LHCI server).workflow_dispatch): optional staging health check, then build with NEXT_OUTPUT=export and CLOUDFLARE_PAGES=1, deploy out/ via Wrangler to Cloudflare Pages. Production and preview environments; custom domain and redirects documented.Docker: Multi-stage Dockerfile (Debian Bookworm slim base, security updates, non-root user nextjs). We copy only .next/standalone, .next/static, and public; start with node --max-old-space-size=512 server.js on port 8000. No healthcheck in the image so Koyeb can own health checks. .dockerignore keeps build context small and excludes dev artifacts and Lighthouse output.
Secrets: No secrets in repo. We use GitHub Actions secrets (e.g. DOCKER_HUB_TOKEN, KOYEB_API_TOKEN, CLOUDFLARE_API_TOKEN_PAGE, CLOUDFLARE_ACCOUNT_ID) and env at runtime (Koyeb env vars for the service). Docs (e.g. KOYEB_DOCKER_HUB_SECRET) describe how to configure registry and deployment.
Headers (production only, in next.config.mjs): Content-Security-Policy (default-src 'self', script/style/img/font/connect tailored, frame-ancestors 'none', object-src 'none', upgrade-insecure-requests), Strict-Transport-Security (max-age=31536000; includeSubDomains; preload), X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, Permissions-Policy (camera, microphone, geolocation disabled). poweredByHeader: false.
CORS: Allowed origin is configurable; we validate Origin for state-changing requests and document allowed methods and headers. Security tests cover CSRF/CORS (origin allowlist, SameSite cookie requirements, POST-only for mutations).
Rate limiting: In-memory rate limiter (per identifier): chat 20 req/15 min. Applied to public APIs. (Scaling to Redis or platform rate limits is documented as a future step.)
Input: Zod for all API inputs; @2toad/profanity for content moderation in chat. SQL is only via Drizzle (parameterized). React escaping and CSP mitigate XSS. We have dedicated tests for CSRF, CORS, session fixation, and request validation.
Health endpoint: In production the health response is { "status": "ok" } only — no stack traces, no internal details.
We treat privacy and regulatory alignment as non-negotiable. The site is designed to be privacy-preserving by default and to comply with GDPR and the EU AI Act (Regulation 2024/1689).
Data minimization and consent: We collect only what is necessary. Chat: anonymous conversations are not persisted; we store messages only when the user submits an email and has given explicit consent (checkbox, logged with the submission). Stored chat data is retained for 7 days then removed. Technical data (e.g. IP, browser) is limited to what is needed for operation. All processing is justified under GDPR Article 6 (consent or legitimate interest). The chat email flow requires an explicit “I agree to data processing” and a link to the privacy policy; the API accepts a consent flag and rejects or does not persist when it is false.
Rights and transparency: The privacy policy (EN/FR) describes what we collect, why, and for how long. Users are informed of their rights (access, rectification, erasure, restriction, portability, objection) and can contact the designated contact (DPO-style) to exercise them. We respond within GDPR timeframes. Cookies: we use only essential cookies (e.g. session, auth); no tracking cookies, no third-party analytics. Cookie preferences and a short explanation are exposed in the UI.
EU AI Act: We align with the Act’s transparency and risk obligations. Users are clearly informed when they interact with an AI system (the chat is presented as an assistant). We maintain (or can produce) technical documentation and human oversight; we do not use AI for prohibited practices (social scoring, manipulative or subliminal techniques, emotion recognition in sensitive contexts). The chatbot is treated as a limited-risk system and meets the transparency requirements (disclosure, no misleading anthropomorphism). Incident reporting is possible via the contact address. Terms and privacy policy both reference the AI Act and disclaim reliance on AI-generated content.
Implementation: Consent is required before persisting any identifiable chat data; the chat API and front end enforce this. Privacy and legal pages are linked from the footer and from consent flows. This is not a one-off compliance pass: any new feature that touches personal data or AI behaviour is designed with GDPR and the AI Act in mind from the start.
We treat Lighthouse as a quality gate. Current targets and results (from local production builds and CI):
Lighthouse scores (typical):
Core Web Vitals:
Improvements that got us here: lazy-loaded ChatSection (~100 KB deferred), server-side structured data, critical CSS inlining, preconnect/preload, font optimization, and aggressive code splitting. Details are in docs/ (e.g. PERFORMANCE_RESULTS_FINAL.md, LIGHTHOUSE_RESULTS_96_PERCENT.md).
No low-code or no-code site builders; no WordPress or generic CMS for the main site. No arbitrary any; no disabling strict TypeScript. No console in production (compiler removes it). No secrets in the repo or in client bundles. No health endpoint leaking internals in production. We do not ship without lint, type-check, and tests in CI.
Direction matters as much as current state. The roadmap is explicit: evolve this site into an AI-native experience, where AI is not a widget bolted onto a classic page but the primary way the product thinks, assists, and adapts. That shift is done incrementally — one step at a time — so each change is shippable, measurable, and reversible.
What “AI-native” means here: The site and its flows are designed with AI as a first-class actor. Content, navigation, contact (via Wagmi, the chat — no separate form), and discovery are shaped so that an assistant can understand context, act on the user’s intent, and improve with usage — without replacing the existing, stable core. Today we have a chat with role-based behaviour and session limits; tomorrow we add richer context (e.g. page, locale, prior messages) and tool use; later we introduce proactive suggestions, summarisation, or guided flows. Each step is a discrete enhancement, with the same quality bar: type safety, tests, security, and performance.
Concrete enhancement steps (illustrative, not exhaustive):
None of this requires a rewrite. The current stack — Vercel AI SDK, Assistant UI, Zod-validated APIs, and a clear separation between server and client — is built to absorb these steps. The roadmap is a sequence of such steps: each one merged, deployed, and validated before the next. The goal is a site that feels AI-native because every addition is designed for it, not retrofitted.
This stack is chosen so that the site itself demonstrates:
The site is fully hand-coded with Cursor [1]: every route, component, and config described above was written and refined in the editor, with AI assistance for implementation speed and consistency, but without sacrificing control over architecture, security, or performance. If you are evaluating technical depth, the repo and the running site are the deliverables to inspect.
Links to the key components of the stack (official sites or GitHub):
Summary of key technologies: Node 20, TypeScript 5.9 (strict), Next.js 16 (App Router), React 19, Tailwind CSS, Drizzle ORM, PostgreSQL, Supabase, Content Collections, Zod, Vercel AI SDK, Assistant UI, next-intl, Radix UI, Biome, Vitest, Playwright, Lighthouse CI, Docker, Koyeb (staging), Cloudflare Pages (production), GitHub Actions.