Case Study
Alina Moments Photography
A full-stack website for a freelance photographer
Solo project: concept, development, production, and maintenance — from the first commit to the live deploy
Starting point
Alina is a freelance photographer in Wolfsburg specializing in newborn, family, couple, and event photography. Before this project she had no website of her own — only an Instagram profile. The goal was a multilingual (DE/EN/RO/IT) professional website with online booking, a contact form, and a moderated review system — no CMS, no agency, no second developer on the team.
Every part of the stack — from the review database to the Content Security Policy to the CI pipeline and deployment — was designed, written, and shipped to production by a single person, held to the same bar a team would set for itself: a dedicated branch and pull request for every change, automated quality gates, end-to-end tests on every merge.
Results — measured live today
- 95/100Lighthouse PerformanceDesktop, measured against the production URL, same methodology as the CI gate: 40ms RTT, 10 Mbps, no CPU throttling
- 100/100Accessibility
- 100/100SEO
- 100/100Best Practices
- 288unit tests, 100% code coverage (statements/branches/functions/lines, Vitest)
- 212end-to-end tests in the CI pipeline (Playwright, 4 browser engines)
- 491commits
- 79merged pull requests
- 436/500 KBshared JS bundle (automated gate in CI)
A methodology note that also appears in the original write-up: on a real mobile network with standard throttling, the performance score swings between roughly 53–73/100 — that reflects the actual cost of high-resolution images on a photography website, not a measurement error. The CI pipeline (production URL, desktop preset, median of 5 runs) consistently lands at 93–96/100.
What's live in production
Online booking through Cal.com (lazy-loaded only on first hover/focus, so third-party cookies aren't loaded unasked), a contact form with Zod validation and email delivery via Resend, page-specific OG images, sitemap/robots, a PWA manifest, native dark mode, and four languages with correct hreflang.
A real, published review
“A shoot at its finest: no stiff poses, just real laughter and real faces. The photos look like they're straight out of a reel, only better. Everything happened in a flash and the result is amazing. These aren't photos, they're real memories. Rating: 10/10”
The full flow is real: the client submits a review through the public form → the system writes a row with status pending to Postgres (Neon) → a moderation email with an HMAC-signed link goes out (one-click approve/reject, no sensitive data in the token) → once approved, the review is published and the homepage is revalidated via revalidateTag. Tested live on June 1, 2026 with a real review, confirmed by the client. Current state of the database (as of September 2026): 5 approved, 1 pending review — a modest but real number, growing organically since go-live, and the infrastructure has stayed verified and production-ready throughout.
Engineering, not just design
A “harmless” Node import broke the CSP on every page.
lib/utils.ts imported Node's crypto.timingSafeEqual in the same file that exports cn() — the CSS class utility used by every client component. As a result, Next.js also bundled the vm-browserify polyfill (which uses eval() internally) into every browser bundle, violating the strict-dynamic CSP. Found through empirical bisection (progressively stubbing components and inspecting the compiled chunks), not by guessing. Fix: moved the function into a server-only module. Lighthouse Best Practices: 78 → 100 (CI).
Rate limiting could be bypassed under concurrency.
The rate limiter ran get and set as two separate steps (TOCTOU) — concurrent requests could slip past the limit before the counter updated. Fix: atomic kv.incr(), for both Vercel KV and the local dev fallback.
Secret comparison wasn't constant-time.
Moderation tokens and admin secrets were compared with === across four different endpoints — theoretically vulnerable to timing attacks. Fix: a central timingSafeStringEqual(), used everywhere a secret is checked.
Two parallel E2E runs were blocking each other.
The concurrency gate in ci.yml compared github.ref (push event) against github.head_ref (pull request event) — two values that never match. Result: every commit triggered two simultaneous E2E runs on the same shared runners, with intermittent timeouts on WebKit (reproduced live: same commit, one run green in 9:42, the other red in 16:51, running at the same time). Fix: github.ref_name instead of github.ref. Verified: the duplicate run is now cancelled within 2 seconds.
API routes were silently redirected under the locale prefix.
The i18n middleware didn't exclude /api/* from its matcher: with localePrefix: "always", every call to /api/admin/reviews was silently redirected to /de/api/admin/reviews (which doesn't exist). Discovered by manually checking that an expected 401 status actually came from the app code and not from Vercel. Fix: explicitly excluded api from the next-intl matcher.
Tech stack
- Next.js 16 (App Router)
- React 19
- TypeScript 5
- Tailwind CSS 4
- Framer Motion
- GSAP + ScrollTrigger
- Lenis
- Drizzle ORM
- Neon Postgres
- Vercel KV
- Cal.com
- Resend
- Sentry
- Vitest + Testing Library
- Playwright
- Codecov
- GitHub Actions
- Vercel Hosting
Honest status, not a mockup
- Live since June 1, 2026 — no staging environment: public domain, real database, real transactional emails.
- No custom domain — a deliberate, confirmed decision by the client, not a technical limitation.
- Single-author repository — all 48 pull requests were opened by the same person, checked by the CI gate, and merged; no second person did code review.
- Review volume is still modest — 5 approved, 1 in moderation: growing organically since go-live, but the flow is not yet tested at real scale.




