
Last updated
Los Lagos Hotel is a family run hotel in El Calafate, the Patagonian town most travellers use as a base for the Perito Moreno glacier. When this work started the hotel was moving off Minihotel, an older property management system, onto Cloudbeds. That migration is what made the rest possible: once the rooms, rates and reservations lived somewhere with a real API, the hotel could finally sell directly from its own site and control its own pricing.
So I built two web apps for it. One is the bilingual site guests book on; the other is the internal tool the hotel prices with. They look nothing alike, and they are not meant to. One is a shop window, the other is a back office. What they share is a backend, and both talk to the same Cloudbeds account.
The money problem was not complicated. A large share of the hotel's bookings arrived through Booking.com and similar platforms, and every one of them carries a commission. The hotel already had a website, so in principle those guests could have booked directly. But the site did not look like somewhere you would hand over a card, and it was not easy to use. Both cost conversions, and the commissions kept being paid.
Selling direct sounds simple until you look at what the hotel has to get right in Argentina. Cloudbeds charges in Argentine pesos, but the hotel and the revenue people who price for it work in dollars, because the peso moves often enough that a price set in pesos is quietly wrong within a week. And guests who live outside Argentina are exempt from the 21% VAT on lodging, so the same room legitimately has two prices depending on where the guest lives.
Both of those had been landing on the reception desk, worked out case by case. So the brief came down to three things: take bookings on the hotel's own site, make that site worth trusting with a card, and show each guest the price that actually applies to them without anyone doing the arithmetic by hand.
The guest site is Next.js with TypeScript, statically pre-rendered in each language. The internal tool is a React single page app built with Vite. Supabase is the shared backend: Postgres, row level security and Edge Functions. Both apps ultimately write to the Cloudbeds API.


The public site is bilingual. Spanish keeps the plain URLs, preserving the paths the hotel's existing site already uses, and English sits under /en. A first time visitor does not have to go looking for it: the site reads the language their browser asks for, routes them accordingly, and remembers the choice in a cookie for a year, with a switch in the header for anyone who wants the other one. Every page is pre-rendered in both languages, so crawlers receive real HTML instead of an empty shell. Beyond the rooms and the hotel itself, most of the content exists to answer the questions people actually ask before committing to a trip to Patagonia: how far the centre is, how to get in from the airport, what there is to do once you arrive.
Two details I particularly enjoyed building. The sections are separated by curved SVG transitions rather than the usual straight rule. And the location page carries an interactive OpenStreetMap map with custom pins for the hotel and for the restaurants, attractions and banks around it, alongside walking and driving times to the centre, the bus terminal, the airport and the glacier. Review scores from TripAdvisor, Google and Booking are gathered onto a single strip so guests do not have to go hunting for them.

The homepage: the hero, the reasons to book direct, and the availability search
The booking page embeds Cloudbeds' Immersive Experience, a third party web component that renders its own checkout inside the site. It is effectively a black box, so everything the hotel needed on top of it is added by a single observer that watches the component's DOM and re-applies the adjustments each time it re-renders. What that layer adds:
The most instructive bug came out of that observer. Driving Cloudbeds' own quantity stepper made the component re-render, which re-triggered the observer, which drove the stepper again. One click produced over 600,000 DOM mutations and froze the tab. The fix was a re-entrancy guard around the adjustment pass, and any code that touches Cloudbeds' DOM now has to run inside it.

The booking engine, with the residency toggle and the bed type selector
Some property management systems bundle revenue tooling that would do part of this. The catch is lock in: the pricing logic ends up living inside someone else's product, and moving away later gets expensive. Flexibility mattered more to this client, so the pricing logic sits in a separate app of their own, talking to Cloudbeds through its API rather than living inside it.
The second app is where the hotel sets its prices. It is internal only. There is no public sign up, and accounts are created by an administrator. The two roles, pricing and hotel staff, are enforced by row level security in Postgres and checked again inside every privileged function, rather than merely hidden from the navigation.
Not automating that last step is the point. A currency like the Argentine peso can move sharply and with little warning, so a system that repriced every room the instant a feed twitched would be a liability rather than a convenience: a bad or purely transient reading would propagate straight into live prices, and the hotel would find out by selling rooms too cheap. So observing a rate never moves a price on its own. It records the rate, recalculates what the new prices would be, and opens a task. Prices move only once a person confirms the rate is genuinely live in Cloudbeds. Cloudbeds does have its own automatic conversion, refreshed twice a day from a global rates feed, but for this hotel that is neither frequent enough nor the right reference: what it bills against is the official Banco Nación rate, not an international mid market one.
The weak point of a human confirmation step is that a human has to notice it. So the same Edge Functions that open an FX task also push it to a Telegram group, through a bot I wrote for the hotel: the old rate, the new rate, whether it came from the automatic feed or a manual entry, and a button that deep links straight into the tasks page of the ops tool. It posts again when someone applies the rate, naming who did it, and if the rate moves back before anyone has acted it posts a cancellation so nobody does redundant work. The notification is deliberately best effort and can never throw. The FX task is already committed by the time a message is attempted, so a missing token or a Telegram outage must not be able to break rate recording.

Dashboard: latest exchange rate, pending tasks and occupancy
Argentina has its own data protection regime, Ley 25.326, supervised by the AAIP, and the sites were built against it rather than retrofitted afterwards. The guest site carries a privacy policy and a cookie policy written to that law, including the Habeas Data rights it grants and how to exercise them, and the booking flow collects only what a reservation actually needs.
Consent is enforced in code, not just described on a page. There is exactly one non essential category on the guest site, analytics, and nothing analytics related loads until the visitor has explicitly opted in. Three things are never gated: the language preference, the booking engine itself, and the record of the choice. That is what satisfies the prior consent rule under Ley 25.326, and GDPR and ePrivacy for the European guests the hotel gets plenty of.
Behind that gate sits PostHog for product analytics and session replay, alongside Google Analytics, so the hotel can see how people actually move through the site and where they drop out of a booking instead of guessing. The internal tool has its own paperwork: a privacy notice for staff, a record of processing activities, and a retention policy that is genuinely implemented, with a scheduled job purging the activity log after 24 months and the Cloudbeds request logs after 90 days.
The two halves have genuinely different requirements. The guest site is public and has to be indexable, so it is pre-rendered per locale. The ops tool has no SEO surface at all and never needs server rendering, so it is a plain single page app talking straight to Supabase. Smaller, cheaper and simpler to reason about.
The backend is the shared half. Anything privileged runs inside Postgres or an Edge Function: the Cloudbeds token, the service role key, the exchange rate writes. Neither browser ever holds a Cloudbeds credential.
The operations tool is in production and the hotel prices with it: repricing is now one number typed once, previewed, published and verified against Cloudbeds, instead of a manual pass across every room type and every date. As of August 2026 the guest site is complete and waiting on the cutover from the hotel's existing site, at which point direct bookings land on it with each guest seeing the price that applies to them.
A fair amount of the work was not code. Because reception is the team actually charging guests, the project also produced Spanish front desk guides for taking payment by pay link: how to check the residency the guest declared, confirm the exchange rate is still current, and reconcile the amount against Cloudbeds before charging anything.
Two apps for one small hotel: a shop window that has to survive Google, and a back office that has to survive a moving exchange rate.
Thank you for reading,
Julio Macias Gonzalez