Back to projects

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

Role
Full-Stack Developer
Timeframe
May 27 – June 1, 2026 (launch) ongoing updates since go-live

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”

— Giuseppe, birthday shoot

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

  1. 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).

  2. 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.

  3. 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.

  4. 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.

  5. 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.

Screenshots

  • Homepage on a wide desktop viewport, above the fold
  • Homepage on a smartphone, full-page scroll
  • Portfolio page with the first works in the grid
  • Services page with packages and pricing
  • Testimonial section on the homepage featuring Giuseppe's real review