# Case Study — Vallès Classics

> I designed, built and deployed the website and full management platform for a cycling event in Catalonia with 200 spots at 20€, integrating Stripe payments, registration management, a discount code system and a responsive admin dashboard. Live at vallesclassics.com.

- Human version: https://www.augustosalazar.com.ar/project/case-study-vall-s-classics
- Author: Augusto Salazar — https://www.augustosalazar.com.ar/llms.txt
- Tags: Full Stack, NextJS, TypeScript, Supabase, Stripe, Webhooks, Admin Dashboard, Payments

```
Stack: Next.js 16 (App Router) · TypeScript · Tailwind v4 · Supabase (Postgres + Auth + Storage) · Stripe Checkout + Webhooks · Resend · Vercel + Cron Jobs
```

## The Brief

A client running an annual cycling event with no system in place. They needed: a public page, online registration with real payments, a dashboard to manage registrants, automated communication (confirmation email), and a discount system for sponsors/early-bird.  

## Architecture Decisions

**Server Components + server-side actions by default.** All sensitive logic (price calculation, capacity checks, discount code validation) runs on the server; the client only shows a preview. _Never trust the total the client sends._

**Hosted Stripe Checkout, not Elements.** Cuts PCI scope to zero and hands compliance off to Stripe. Idempotent webhooks with dual TEST/LIVE support on the same endpoint (signature verification loop).

**RLS-first in Supabase.** Every table has explicit policies. The service role is only used for audited bypasses in internal endpoints.

**Free-checkout shortcut.** If a 100% discount code brings the total to 0€, the flow skips Stripe entirely and marks the registration as paid, with no pointless checkout session.  
  
![](https://www.augustosalazar.com.ar/images/valles-inscripcion.jpg)  

![Registration flow: the server computes the price, zero-total shortcut, Stripe Checkout and webhook](https://www.augustosalazar.com.ar/images/valles-checkout-en.png)

**Caption:** The server decides the price; Stripe only charges when there is something to charge.

## Key Features
![](https://www.augustosalazar.com.ar/images/valles-features.png)![](https://www.augustosalazar.com.ar/images/valles-admin-codigos.jpg)  
Production Issues I Solved

**1\. A webhook silently broken in production.** Vercel redirects `vallesclassics.com` → `www.vallesclassics.com` (HTTP 307). Stripe **does not follow redirects** on webhooks → 42 payment events were going nowhere. 15 people had paid 20€ and didn't show up in the system. _Diagnosed via Stripe Event Logs, fixed by updating the endpoint URL, recovered by using "Resend" on the events._

**2\. RLS gotcha with** `@supabase/ssr`**.** The service role client was sending the user's cookie JWT in the Authorization header, which overrode the role → upsert blocked by RLS even though the service role "supposedly" bypasses everything. Fix: separate the anon client (auth check) from the service client (the operation itself). Documented in CLAUDE.md.

**3\. Specificity bug in the mobile sidebar.** An inline `style={{ display: 'flex' }}` beat `className="max-md:hidden"` on CSS specificity → the desktop sidebar stayed stuck on mobile. Migrated every display rule to Tailwind classes.

**4\. App settings upsert without** `onConflict`**.** Every Stripe mode toggle inserted a new row instead of updating the existing one → the toggle "forgot" its value after redeploys. Fixed with `onConflict: 'key'` + a SQL dedupe + a hardened schema.

**5\. Credential rotation under pressure.** A mass key leak on Vercel: coordinated rotation of the Supabase service role, Stripe (test + live), Resend and webhook secrets. Documented a zero-downtime migration plan.  

![Incident: Vercel answered with a 307 and Stripe did not follow it; 42 events and 15 payments lost](https://www.augustosalazar.com.ar/images/valles-webhook-en.png)

**Caption:** A redirect invisible to the browser was a black hole for Stripe.

![The full Vallès Classics landing page, top to bottom in three columns](https://www.augustosalazar.com.ar/images/valles-landing.jpg)

**Caption:** The full landing page, top to bottom: read it left to right.

![Vallès Classics on a phone: three moments of the same page](https://www.augustosalazar.com.ar/images/valles-mobile.jpg)

**Caption:** The same page on a phone, built for the thumb.

## Technical Details

-   **Shared discount validation** between the public (preview) endpoint and the server-side checkout — a single source of truth in `lib/discount.ts`
    
-   **Code usage is incremented** only after a confirmed `checkout.session.completed` (not when the session is created)
    
-   **Destructive delete modals** require literally typing `ELIMINAR`; selective deletes use a lighter confirmation because the scope is explicit
    
-   **Transactional email with sponsors** rendered server-side: an HTML table (not flexbox) with CSS filters so logos render white on dark, compatible with Gmail/Outlook
    
-   **Keep-alive cron** because the Supabase free tier pauses the DB after 7 days of inactivity → a daily ping that writes to a log table visible in the admin  
    ![](https://www.augustosalazar.com.ar/images/valles-admin-sistema.jpg)
    

  
Results

-   **Real registrations processed** with payments via Stripe LIVE
    
-   **Zero downtime** during the test → production migration
    
-   **Admin dashboard used by the client from mobile** with no support needed
    
-   **Average registration time**: ~90 seconds from landing to confirmed payment
