Case study CV Next / Behind the build
§ 00 Case study

One idea.
A whole ecosystem.

CV Next is a recruiter-facing career site — a public portfolio with a sign-in-protected CV PDF, a crawlable blog, and an AI concierge — shipped as one container behind Traefik on a ~€10-a-month box. It's also the page you're reading this on. Here is what it's built from and the engineering decisions behind it, so the site itself is the proof of work.

1
container, one port 443
~100
backend tests, JUnit 5
0
tools on the AI model
~€10
per month, self-hosted
The application path

Four products.
One application.

Browser Portfolio · Blog · BuildCV · FOCUS
Traefik TLS termination · :443
cvnext container Spring Boot + React static · /api/**
PostgreSQL Visits · blog · FOCUS data
§ 01

Single-origin deployment

The React bundle is built into Spring's static/ directory and served by the same jar, so the browser hits /api/** same-origin — one container, one port 443, no CORS in production. A multi-stage Dockerfile builds the SPA, then copies it into the Spring image.

Trade-off Couples frontend and backend release cadence, in exchange for radically simpler ops and zero cross-origin surface. For a one-owner site that trade is clearly worth it.

§ 02

Sign-in, brokered

A self-hosted Keycloak on its own subdomain sits between the app and LinkedIn, so LinkedIn's non-conformant OIDC is the broker's problem rather than the application's — the three workarounds this codebase used to carry (nonce stripped, token_type defaulted, PKCE disabled) are deleted, and PKCE is back on. cvnext is a plain OIDC client, but its ClientRegistration is still built programmatically with every endpoint written out rather than discovered: issuer discovery is a blocking call made at bean creation, and since both start together on one box, that would turn a slow identity server into a failed application context.

Trade-off Running an identity server is real operating cost — another process to patch, and a realm that is durable state rather than disposable config. It buys provider quirks that stay out of application code, and a client that still boots with no credentials and with the broker down: only sign-in fails.

§ 03

An AI concierge that is a pure text generator

"Ask Zakaria" lets signed-in recruiters ask up to 20 questions about the profile, answered by DeepSeek via Spring AI and streamed back as SSE. Because it answers untrusted, jailbreak-prone input, the model is given no tools: no ToolCallback, no @Tool, no MCP starter on the classpath — so even a perfect jailbreak yields only words. Answers render as escaped React text nodes, never dangerouslySetInnerHTML.

Trade-off No agentic actions — but an untrusted-input LLM with zero reach into the filesystem, shell, database, or network is exactly the point. Security by construction, not by prompt.

§ 04

Owner-gated authorization, email as identity

One privileged user — the owner — is identified by matching the authenticated OAuth email against a configured owner-email. /api/analytics/** is owner-only (403 otherwise), /api/cv/** needs any authenticated user, and the public endpoints stay public. /api/** returns 401, not a login redirect, so the SPA drives the login flow itself.

Trade-off Email-as-identity is simple and needs no user table, but it binds authorization to an external provider's claim — acceptable for a single-owner site; a product would model roles.

§ 05

A crawlable blog on the same jar, separate schema

blog.zakaria.lu is served by the same container: a host-dispatch filter routes the blog host so the SPA keeps the apex. The blog is server-rendered and crawlable (the dossier is gated and isn't); posts live in a dedicated blog Postgres schema. The owner authors in a TipTap editor whose HTML is re-sanitized server-side (OWASP) before it's persisted — raw editor HTML is never stored.

Trade-off One deploy unit instead of a separate service — simpler ops, at the cost of sharing a process and a database between two otherwise-independent surfaces.

§ 06

Agent-built, hardened production infrastructure

CI/CD, the Traefik reverse proxy and TLS, container hardening (non-root uid, no-new- privileges, a read-only Docker socket), key-only SSH, fail2ban, and a swapfile on a Hetzner CX22 with on-box Postgres were all built with agentic coding (Claude Code) — real infrastructure, not a demo.

Trade-off Self-hosting on a single box trades managed-cloud elasticity and SLAs for full control and near-zero cost — a deliberate choice for a personal site, and a way to keep operating muscle sharp.

The full architecture & trade-offs, decision by decision →

§ 07 What this demonstrates

Backend depth, security-first instincts, and end-to-end delivery.

One person taking a real product from a typed config surface and race-safe persistence, through secure-by-design auth and AI, to hardened infrastructure and CI/CD — and keeping it honest enough to publish the trade-offs. Built with the same tools I'd bring to a team.

Java 25 Spring Boot 4 Spring Security Spring AI OAuth2 / OIDC PostgreSQL 17 Flyway React 19 TypeScript Vite Thymeleaf Docker Traefik Hetzner GitHub Actions