Los Lagos Hotel logo

Los Lagos Hotel

Last updated

Introduction

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 Brief

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.

Technologies

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.

Next.js icon
React icon
TypeScript icon
Supabase icon

Site One: The Guest Website

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.

Los Lagos Hotel homepage hero with the availability search

The homepage: the hero, the reasons to book direct, and the availability search

Making the Booking Engine Fit the Hotel

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:

  • A residency toggle. The guest states whether they live in Argentina or abroad, which decides whether the 21% VAT applies to what they see, and writes a billing flag onto the reservation so the exemption is applied downstream instead of reception capturing nationality by hand.
  • Prices that trace back to dollars. The hotel's durable price is in dollars while the guest is charged in pesos, and the conversion runs on a rate the site fetches through an API route with a sanity band, a freshness limit, a last known good fallback and a degraded mode that declines to convert at all rather than use a guessed rate.
  • A bed type selector Cloudbeds does not offer. Guests choose a double bed or two singles per room, with counters bounded by what is genuinely free that night. The physical room IDs behind that calculation stay on the server and never reach the browser.
  • A recorded price breakdown. Net, VAT and gross in both currencies, plus the exchange rate used, are written onto each reservation as custom fields, so the front desk charges from numbers captured at booking time rather than recomputing them days later.
  • Automatic room assignment. When a booking completes, a webhook reads the bed layout the guest asked for and assigns matching physical rooms. It deliberately leaves Cloudbeds' own assignment alone when nothing compatible is free, rather than putting someone in the wrong bed.

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.

Cloudbeds booking engine with the residency toggle and bed-type selector

The booking engine, with the residency toggle and the bed type selector

Site Two: The Rate Operations Tool

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.

  • A price calendar that works in dollars. The hotel brings in outside revenue managers, and dollars are the currency they price in, so dollars are what they type: one base price for a single day or for a whole date range, or a bulk upload by CSV when a full season is being repriced at once. Each room type derives its own price from a percentage modifier, rounds up to the next twenty cents and converts to pesos at Cloudbeds' own rate. Pricing in the currency they actually think in is what makes smart pricing practical here at all.
  • Publishing that checks its own work. Publishing runs as preview then commit. The preview shows exactly which rows will be sent and what Cloudbeds will receive, and after committing the app reads the rates back out of Cloudbeds and marks each one confirmed, sent or failed depending on what came back.
  • An exchange rate watcher. A scheduled function reads the official Banco Nación rate, records it only when it has actually changed, recalculates every stored peso price from its durable dollar source, and opens a task for staff. That task cannot be closed by simply clicking a button. Completing it re-reads Cloudbeds' currency settings and only succeeds when they match.
  • The operational surround: an activity log, team management, a help section written for hotel staff rather than developers, and account security with two factor authentication and passkeys.

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.

Rate Ops dashboard with the latest USD/ARS rate and pending tasks

Dashboard: latest exchange rate, pending tasks and occupancy

Privacy, Consent and Analytics

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.

Why Two Apps and Not One

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.

Where It Landed

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

Contact me