
Capabilities
- Live track days from TrackLap
- Back office
- CDN images
- Transactional email
Frontend
Backend
Sport Moto Team
In ProgressSport Moto Team is the Yamaha Official Riding School: over fifteen years running motorcycle track days across Spain. Their site lived on an old platform and I rebuilt it as a Next.js 14 project with the App Router and server components: a home page with the next track day and a countdown, monthly and yearly calendars, rental, annual insurance, guestbook, media and contact. Track days are never typed by hand: they are read from TrackLap's public API, where the club already manages them. Behind it sits a panel where the team edits copy, banners, sponsors, photos and reviews, on PostgreSQL with images served from Cloudinary.
The challenge
The club already manages its track days on TrackLap, so the website could not become a second place to maintain the same dates, seats and prices: it had to read them from there and stay current without anyone touching anything. Everything else — copy, photos, sponsors, reviews — is changed by the club itself, often from a phone at the side of the track, so the panel had to be comfortable on a touch screen and never need a developer for a change. And the same codebase had to deploy just as well on Vercel as on their own VPS.
Screenshots

Desktop 
Mobile
Architecture
Architecture diagram for Sport Moto Team
Implementation
Live track days from TrackLap
The home page, the calendar and the next-event countdown are fed by TrackLap's public API through server components revalidated every five minutes; the booking button leads to the enrolment on TrackLap. A contract test compares the real production response against the local types so a backend change breaks the build before it reaches the site.
The database owns the content
Every piece of copy is a row in content_blocks: the literal in the code is only the fallback when the row is missing. The club edits by section with a preview, or by clicking the text on the live site in edit mode, across all eight pages.
Multi-user panel with signed sessions
Owner and editor roles, scrypt-hashed passwords and a session in an HttpOnly cookie signed with HMAC-SHA256. The middleware runs on the Edge and only checks signature and expiry; the authoritative check happens in every API route on Node, and changing a password or deactivating someone expires their sessions instantly by bumping their token_version.
Brute-force throttle and cold start
Five failures per IP and user lock the login for fifteen minutes. If the users table is empty, the first login with the environment password creates the owner; from then on the database rules and that variable is never used again.
Moderated guestbook with fifteen years of history
New reviews start hidden and are published from the panel, with an optional pinned position, and the 660 reviews from the old site (2011–2026) are imported idempotently by their original number.
Right-sized images from Cloudinary
The panel uploads photos, banners and sponsor logos to Cloudinary; URLs are rewritten with f_auto, q_auto and the exact width each slot needs, and the home page went from about 9 MB down to under 1 MB of images.
Forms that never get lost
Annual insurance and guestbook submissions are saved to the database first and only then emailed to the club with Nodemailer: if the email fails, the request is still there and visible from the panel.
One codebase, two deployments
The database client picks its driver from DATABASE_URL: Neon's serverless HTTP driver on Vercel, or pg with a pool against a plain Postgres on the VPS with EasyPanel, packaged with a Dockerfile. Tests swap the database for a fake in-memory Postgres: none of them touches a real one.
A panel built for the phone
It is used at the side of the track: nothing essential hidden behind a hover, 44 px touch targets, two-step confirmations instead of window.confirm and a sticky save bar showing how many changes are pending.