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
- PG
- backend tests on disposable PostgreSQL
- 0
- tools on the AI model
- ~€10
- per month, self-hosted
Four products.
One application.
Single-origin deployment
The React bundle is packaged into the Play distribution and served by the
same application, so the browser hits /api/** same-origin in production.
A multi-stage Dockerfile builds the SPA before packaging it with the Scala backend.
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.
Sign-in, brokered
A self-hosted Keycloak on its own subdomain sits between the app and LinkedIn, so LinkedIn's OIDC quirks stay in the broker. The Play backend wires pac4j with explicit endpoints, PKCE S256, state and nonce, without fetching issuer metadata at startup. A slow identity server therefore affects sign-in rather than public-site startup.
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, while the site still boots without credentials or an available broker: only sign-in fails.
An AI concierge that is a pure text generator
Anyone can use Ask Zakaria or Match a role without signing in or providing an email.
Twenty successful questions are shared across both modes per browser cookie or signed-in
account, with a temporary IP guard on generation attempts. Play WS and Pekko Streams send
text to DeepSeek and stream SSE answers. The model has no tools, function
calls or access to analytics data. 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.
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 an authenticated human, and the public dossier
and Concierge endpoints stay public. Protected /api/** routes return 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.
A crawlable blog on the same jar, separate schema
blog.zakaria.lu is served by the same Play container: host routing sends the blog
host to Twirl while the public React dossier keeps the apex. The blog is server-rendered
and crawlable; posts live in a dedicated blog PostgreSQL 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.
Agent-built, hardened production infrastructure
CI/CD, Traefik routing and TLS, container hardening (non-root UID, no-new-privileges, a restricted Docker socket proxy), key-only SSH and PostgreSQL run on a Hetzner box. An OpenTelemetry agent traces each request from Traefik through Play to every SQL call into a self-hosted Grafana stack (Tempo, Prometheus, Loki) that nothing depends on. Zakaria built and operates this infrastructure with coding-agent assistance.
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.
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.