Case Study · E-Commerce
🍺 Buy me a beer

CuevasLab Shop & CMS from scratch.

A full-stack e-commerce platform built from scratch with Claude Code and The Copilot Loop. Backend with Medusa.js, storefront with Next.js, CMS with Strapi — all self-hosted on a Hetzner VPS, deployed with GitHub Actions, and serving content in 6 languages.

6 countries
6 languages
+300 tests
5 pipelines CI/CD
Online store Backoffice CMS

All these years evolving other people's products... Why not build my own?

This project didn't start with a technical requirement. It started with a question I took too long to ask: if I spend my career deciding what to build — why have I never built something completely my own? This, combined with the curiosity to explore AI-assisted development, led me to build a website and online store from scratch. Not to sell real products, but to prove to myself that I could build and maintain a full e-commerce stack — with AI as copilot, but with real architecture, infrastructure, and code decisions.

01

A portfolio. A hidden CV. A starting point.

Simple idea first: buy a domain, build a landing page and a password-protected CV. Simple enough to barely mention — and exactly what was needed to prove the workflow worked.

02

Mini-games as a methodological test.

Once the base was live, the question changed: can I apply what I know about structured product development to AI-assisted coding? The homepage mini-games weren't just fun — they were the first real test of what would later become The Copilot Loop.

03

March 8th. An email changed the scope.

"For 24 hours, building in Lovable is completely free, in collaboration with Anthropic." What more could I ask for? I spent that day designing what MY complete e-commerce would look like, built entirely my way.

What I wanted to prove with this project.

That I could dust off my "techie" background to run a complex, multi-service e-commerce stack from zero to production — making real architectural decisions and defining my own way of working.

Three independent, deployable services

Commerce backend, headless CMS, and storefront as three separate services, each with its own pipeline.

Multilingual UI with a pragmatic strategy

i18n in 6 languages with a strategy that survives the framework's constraints.

Reproducible infrastructure on cheap hardware

Everything runs on a VPS costing less than €5/month. Documented for installation from scratch.

Six containers. Two platforms. One monorepo.

The stack splits cleanly between the VPS (stateful services, admin interfaces, CMS) and Vercel (edge-delivered storefront). Traffic enters through Nginx, which routes to the Medusa API or the CMS API. Stateless content is cached in the Next.js server layer with tag-based invalidation.

Full-stack architecture: GitHub Actions pipelines, Docker Hub, Hetzner VPS with Nginx, Medusa, Strapi, PostgreSQL, Redis, and storefront on Vercel

Client browser

Accesses the Next.js storefront served by Vercel — edge-delivered, fast, globally cached.

Vercel · Next.js

Server components fetching from Medusa and Strapi APIs. Content cached with Next.js tags.

Strapi CMS

Headless CMS managing homepage content in 6 locales. Schemas defined as code. REST API on port 1337.

Nginx gateway

TLS termination and reverse proxy. Routes shop-backoffice.* to Medusa and cms.* to Strapi.

Medusa.js v2

Commerce engine with product catalog, checkout, and admin. Custom seed scripts for the demo catalog.

PostgreSQL · Redis

Shared persistence for Medusa and Strapi. Separate databases, same Docker network.

Architecture decisions

Why CMS and commerce are separate services

Medusa handles transactional data — products, orders, checkout. Strapi handles editorial content — hero text, category descriptions, announcements. Keeping them separate means each can be updated, restarted, or scaled without affecting the other.

Why self-hosted instead of managed SaaS

Hetzner CX23 costs less than €5/month and gives full stack control. The infrastructure overhead is worth it to demonstrate operational ownership — the ability to configure Nginx, manage SSL, and debug Docker networking.

Vercel for the storefront, not for the backend

Next.js on Vercel gets edge delivery, automatic preview deployments, and zero server maintenance. The commerce backend and CMS need persistent storage and admin UIs — those belong on the VPS.

Content in 6 languages, defined in code.

Strapi v5 runs in production mode, which disables the Content-Type Builder UI. All schemas are committed as JSON files and registered with factory-based controllers and routes. Content is fetched server-side in Next.js with locale fallback and cached per model with tag-based invalidation.

1

Editor publishes in Strapi Admin

cms.cuevaslab.es/admin — localized fields for each of the 6 supported languages.

2

Strapi REST API serves localized content

strapiGetLocalized() fetches with locale param, fallback to EN if locale doesn't exist.

3

Next.js server components cache with tags

Each fetch includes next: { tags: ["hero"] }. Responses are cached at the server layer.

4

Cache invalidation via revalidate endpoint

POST /api/revalidate?secret=... with model UID triggers revalidateTag() for that content type only.

The decisions that shaped the stack.

Three non-obvious decisions made during construction. Each arose from a real constraint or failure, not upfront design. The context behind each decision matters more than the decision itself.

Cookie-based i18n instead of next-intl

Medusa's storefront uses middleware to inject the country code into the URL. next-intl also runs as middleware. Two middleware functions in Next.js conflict — only one can intercept the request. Cookie-based i18n with getLang() sidesteps the conflict entirely.

Strapi schemas in code, not in the admin UI

Strapi v5 production mode disables the Content-Type Builder. You can't create or modify content types from the UI after deployment. Schemas live as JSON files in src/api/[name]/content-types/[name]/schema.json. This is better: versioned and reproducible schemas.

Nginx with lazy DNS resolver for Docker services

Nginx resolves upstream hostnames at startup. When Strapi's container starts after Nginx (which happens in Docker Compose), Nginx fails with "host not found in upstream" and enters a crash loop. Fix: use Docker's internal DNS server (127.0.0.11) with proxy_pass in a variable so resolution happens at request time, not boot time.

A tool ecosystem. Each one chosen for a reason.

Setting up an e-commerce isn't just picking a framework and starting to code. It's choosing twenty tools that need to work together, and each choice has consequences on cost, performance, and maintenance. These are the decisions we made and why.

Commerce engine

Medusa.js v2 — The decision that changed everything

The question was: Shopify, WooCommerce, Magento, or something headless. Shopify is SaaS — no real control over the stack. WooCommerce is tied to PHP and WordPress, a world I know well but wanted to leave. Magento is a giant for a one-person project. Medusa.js v2 offered what I needed: headless, open source, Node.js, API-first, and a fast-growing community. And free.

Infrastructure and hosting

Hetzner vs Azure — The reality of cost

I evaluated Azure first — it's what we use at work. But for a personal project with Docker Compose, PostgreSQL and Redis, Azure came out to over €40/month just for compute and storage. Hetzner CX23: less than €5/month for a VPS with 4GB RAM, 40GB SSD, and traffic included. Full stack control for a tenth of the price. The decision was obvious.

Vercel — Free... until you're not

Next.js on Vercel is a natural combination: edge delivery, preview deployments, zero config. The free tier was perfect at first. But as the storefront grew — ISR, Server Components, API routes — Functions CPU usage started approaching the limit. We're evaluating alternatives (Cloudflare Pages, self-hosted) because Vercel's bill can scale fast once you leave the free tier.

Cloudflare — DNS and protection

DNS management, proxy, CDN-level caching, and basic DDoS protection. All free. Cloudflare is one of those tools that once you configure it, you forget it's there — and that's exactly what you want from your network layer.

CMS and content

Strapi v5 vs Contentful vs Sanity

Contentful and Sanity are cloud-hosted — easier to set up, but with pricing that scales by API calls and locales. With 6 languages and growing content, the cost skyrockets. Strapi v5 is open source, self-hosted on the same VPS as Medusa, and gives me full control over schemas. The trade-off is that I maintain it — but that's exactly what I want to demonstrate.

Cloudinary — Optimized images

Product images are served from Cloudinary with automatic transformations: WebP/AVIF format, viewport resize, lazy loading. The free tier more than covers the demo catalog needs. And it significantly improves LCP versus serving images from the VPS.

Integrations and services

Google Analytics + Tag Manager

GA4 for user metrics and GTM to manage all third-party scripts without touching code. Each new service (cookies, reCAPTCHA, clarity) is added via GTM, not hardcoded in HTML. This keeps the code clean and tracking centralized.

Cookie Script — GDPR consent

Cookie banner with type categorization (necessary, analytics, marketing). GDPR compliance without implementing anything custom — the script is injected via GTM and manages consent automatically.

reCAPTCHA v3 + v2 fallback

Bot protection on contact and registration forms. reCAPTCHA v3 is invisible — runs in the background and scores the risk. If the score is low, the v2 checkbox appears as fallback. The best UX possible without sacrificing security.

Google Translate — Catalog translations

Editorial content (CMS) is manually translated to 6 languages. But product descriptions in Medusa use Google Translate API as a base, reviewed afterwards by a human. Pragmatism: a decent translation is better than an empty description in 5 of the 6 languages.

Resend — Transactional emails

Order confirmations, password resets, notifications. Resend offers a clean API, good free tier, and templates with React Email. Integrated directly with Medusa via custom notification provider.

The challenge: €0 budget. Real production.

When I decided to set up the e-commerce, I gave myself an absurd challenge: spend as little as possible. Ideally €0. We all know it's impossible — the domain already costs something. But the idea was to prove that with the right tools, a cheap VPS, and plenty of free tier, you can build a serious platform without significant investment.

And then March 8th arrived. Lovable announced 24 hours of free usage in collaboration with Anthropic. No limits. Any design you wanted, free. For an e-commerce product owner with a full stack in his head, it was like leaving a kid alone in a candy store. I spent the entire day designing: from the base design system to the last checkout page. All the designs that Claude Code would later turn into real code.

Real monthly cost

Hetzner VPS: ~€5. Domain: ~€1/month amortized. Cloudflare, Cloudinary, Vercel, Strapi, Medusa, GitHub Actions: €0 (free tier). Resend: €0 (free tier). Total: less than €6/month for a complete e-commerce stack in production with 6 languages.

Lovable as a design accelerator

March 8th wasn't just a day of free design — it was the moment the project went from "a portfolio with a basic store" to "a complete e-commerce platform with a design system, 20+ designed pages, and a clear product vision". Without Lovable, those designs would have taken weeks.

Built from scratch with an AI code agent.

This project was built using Claude Code and The Copilot Loop — a structured 10-phase methodology. Claude Code operated as a senior engineer: reading files, executing commands, writing code, and committing changes. The human role was architecture decisions, product direction, and validation.

What Claude Code executed

Strapi v5 content types

JSON schemas, controllers, services, and routes for the 5 content types.

Cookie-based i18n system

getLang(), t(), SUPPORTED_LANGS, countryFlag() utility.

GeoModal

Browser locale detection → country + language selection + cookie persistence.

5 GitHub Actions workflows

Path-triggered builds, push to Docker Hub, SSH deploy to VPS, Vercel deploy.

Nginx configuration

TLS routing, lazy DNS resolver, upstream definitions for Medusa and Strapi.

What stayed human

Stack selection and architecture decisions

Medusa vs Shopify, Strapi vs Contentful, Hetzner vs managed cloud. Service boundaries, what goes on VPS vs Vercel, cost constraints.

CMS content and product strategy

All content written, structured, and published by me in Strapi admin. What to build vs defer (Stripe, dark mode, wishlist), demo scope.

Debugging judgment

Forming hypotheses, deciding when to escalate, root cause analysis.

Real metrics. Not promises.

Every storefront deploy runs Lighthouse CI automatically — both in staging and production. Results are stored and visualized with historical trends in the deployment dashboard. These are the real numbers from the latest deploy.

Metric Score Target Status
Performance 85–92 ≥ 85
Accessibility 95–100 ≥ 90
Best Practices 95–100 ≥ 90
SEO 92–100 ≥ 92
LCP < 2.5s ≤ 2500ms
CLS < 0.1 ≤ 0.1

Lighthouse CI on every deploy, not just once

Lighthouse runs in GitHub Actions after every deployment. Thresholds are set to "warn" (not "error") because CI runners don't replicate real user conditions — they serve to detect regressions, not as absolute values. The exception is CLS, which is an error because it measures layout shifts regardless of runner speed.

Historical trends per release

Each release has its metrics stored. The deployment dashboard shows the evolution: if the Performance score dropped 5 points in v1.20, it's immediately visible. This allows detecting which change introduced a regression.

Key optimizations

Cloudinary preconnect for hero LCP. Fonts with display: swap. Images in WebP/AVIF. Lazy loading on below-the-fold. CSP with strict-dynamic for GTM. ISR on catalog pages to reduce CPU time in Vercel Functions.

PageSpeed Insights mobile capture for storefront homepage with scores 90 Performance, 93 Accessibility, 92 Best Practices and 100 SEO.

Real PageSpeed Insights capture for the storefront's mobile homepage. This block will later grow with the equivalent Lighthouse capture to complement the comparison.

Five independent pipelines. One monorepo.

Each sub-project has its own GitHub Actions workflow, triggered only when its files change. A push to web/ doesn't trigger a storefront deploy. This keeps CI fast and avoids unnecessary rebuilds.

Pipeline Branch Trigger Steps
deploy-web main web/** npm ci → build → Vercel deploy
deploy-storefront main storefront/** validate → Vercel deploy (prod) → E2E → Lighthouse
deploy-storefront-staging develop storefront/** validate → Vercel deploy → alias preprod → E2E → Lighthouse
deploy-cms main cms/** Docker build+push → SSH deploy VPS
deploy-shop main shop/** Docker build+push → SSH deploy VPS

Four sub-projects, one repo, clear ownership.

Each directory is independently deployable with its own CI/CD pipeline. Adding a new service means adding a directory and a workflow — no shared build state, no cross-contamination.

/web

  • Portfolio and case studies
  • Static HTML + design system CSS
  • i18n via data-i18n attributes
  • Dark/light mode, draw.io diagrams

/storefront

  • Customer-facing Next.js store
  • App Router (TypeScript)
  • Medusa + Strapi API clients
  • Cookie-based 6-language i18n system

/shop

  • Commerce backend and infrastructure
  • Medusa.js v2 + Docker Compose
  • Nginx config + seed scripts
  • PostgreSQL + Redis containers

/cms

  • Headless CMS for storefront content
  • Strapi v5 (TypeScript)
  • Content types as JSON schemas
  • i18n: ES, EN, DE, FR, IT, PT

Open the live stack.

Everything described here is live and publicly accessible. The source code is the main artifact — the deployed services let you verify it actually works.

Let's talk

Drop me a note — questions, feedback, or just want to say hi.

Message sent! I'll get back to you soon.

Something went wrong. Please try again.