← All workCase study · Web application

Solè Positano

Booking for a beach club and restaurant in Positano. Customers book sunbeds, tables and packages and pay online. The same platform now runs two more venues.

Role
Frontend lead at PixelCrew
Timeline
Jun 2024 – Jul 2026
Team
PixelCrew backend team (Laravel)
Live
booking.solepositano.com

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.

The Next.js frontend queries a Laravel GraphQL API backed by MySQL and Redis. Payments go through Nexi, emails through Brevo, receipts through a fiscal gateway.Booking frontendNext.js · Apollo · codegencheckout middlewareGraphQL APILaravel · Lighthousebackend teamDataMySQL · RedisBack officeLaravel NovaGraphQLNexipay off-site, return with outcomePayment controllernonce · capture · errorsBrevoemailFiscalreceiptsredirectbuilt by meowned by others
The customer leaves the site to pay on Nexi and comes back with the outcome in the URL. On the backend I contributed to the payment controller. 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:

middleware.ts
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.