Problem
Solè is a beach club and restaurant in Positano. It used a third-party booking system. The new platform brings booking in house. Customers book experiences and packages, such as sunbeds and tables, and pay online. They can also buy gift cards, join a waiting list and open their booking from a link with its code. The staff manages sunbeds, the restaurant, the opening calendar and payments from a back office.
Architecture
The browser talks to a Next.js app, which queries a GraphQL API built with Laravel and Lighthouse. The API stores data in MySQL and Redis. Payments go through Nexi: the customer leaves the site to pay and comes back with the outcome in the URL. Emails go through Brevo, and receipts go through a fiscal gateway. The staff uses a Laravel Nova back office. When content changes, the backend calls a revalidate endpoint on the frontend.
Scope
Mine
- The booking frontend for Solè and for the other venues built on it
- Typed GraphQL queries and mutations, generated from the backend schema
- The checkout and the return flow from the payment gateway
- Part of the Nexi payment backend: a nonce before the redirect, a guaranteed capture and error handling
Other teams
- GraphQL API, data model and the Nova back office (PixelCrew backend team)
- Infrastructure and deploy
Key decisions
Generate the types from the backend schema
Why. The API is written by another team and changed during the project. Codegen reads the GraphQL schema and turns every query into typed hooks. A removed field breaks the build, not the checkout.
Trade-off. The schema has to be reachable during development and at build time.
Clean the payment return URL in middleware
Why. After paying, Nexi sends the customer back with parameters in the URL. The middleware keeps only the parameters the page needs, such as the outcome and the transaction code, and redirects to a clean URL. Anything else is dropped before the page runs.
Trade-off. A new gateway parameter has to be added to the list by hand.
Send the purchase event once, and never block the customer
Why. The flow jumps between the site and the payment gateway, so a conversion can be lost or counted twice. The purchase event waits until the tags stop answering for 300 ms. If the tag manager is blocked, a hard timeout moves on anyway. A flag in storage stops a second event on reload.
Trade-off. The confirmation page waits up to the timeout before it continues.
In code
The middleware that cleans the payment return URL:
const CHECKOUT_PAYMENT_ALLOWED_PARAMS = [
"alias",
"codTrans",
"importo",
"divisa",
"esito",
"product",
] as const;
function stripDisallowedParams(
request: NextRequest,
allowed: readonly string[],
) {
const url = request.nextUrl;
const hasDisallowed = [...url.searchParams.keys()].some(
(k) => !allowed.includes(k),
);
if (!hasDisallowed) return null;
const cleanUrl = new URL(url.pathname, url.origin);
for (const key of allowed) {
const value = url.searchParams.get(key);
if (value !== null) cleanUrl.searchParams.set(key, value);
}
return NextResponse.redirect(cleanUrl);
}One platform, three venues
The same booking platform now runs two more venues. Chic Mama has its own copy of the backend. Arienzo Beach Club uses the Solè backend and adds English and Italian. Each venue is a separate fork, adapted to its own packages and rules, not a multi-tenant system.
Results
- 3
- venues on the same booking platform
- 630+
- versioned releases of the Solè frontend
- 987 / 994
- commits on the frontend are mine
Solè now takes bookings and payments on its own platform, without the third-party system. I can’t share usage numbers. The frontend reached version 1.0.632, and the same code became the base for two more venues.