02 of 04

Giuvala by BNZ

Two platforms and a telephone, all able to sell the same weekend.

Guest house at 1290 metres, rented whole. Next.js, Firestore, iCal, Claude.

Seven and a half seconds down the homepage, held still at each end so the hero video reads as video. Every frame on this page was taken from a throwaway copy of the build, served with no credentials of any kind: the public pages render the Romanian text shipped in the source, the phone number in the navbar is an invented substitution, and every booking and every blocked night further down this page was invented for the photographs.

A house of eight rooms at 1290 metres, at the top of the Giuvala pass, rented whole rather than by the room. It is listed on Airbnb and on Booking.com and it also takes bookings over the phone, which is three calendars that do not know about each other and one weekend between them to sell. The site merges all three into one set of blocked nights and publishes the phone bookings back out as a feed the two platforms subscribe to. An assistant sits on every page and answers in Romanian, and dates are the one thing it is forbidden to answer from memory: it calls the same availability check the booking form calls, and reads back what it is told.

The problem

A property listed on Airbnb and on Booking.com has two calendars it does not control. Add the guests who ring up and there is a third, which lives in somebody’s head. Each of the three can sell the same weekend, and the house is rented whole, so there is no spare room to move anybody into when two of them do.

The second problem is quieter. Most of what a guest wants to know is already written on the site, and it still arrives by telephone, at whatever hour it occurs to the guest. Both questions were being answered by hand, and the one about dates cannot be answered wrong.

What we built

  1. 01

    Twelve pages, in Romanian

    The public site is twelve pages: the eight rooms one after another, the facilities, the gallery of 75 photographs in three sets, the attractions and the activities around the pass, its history, the page about the owners, the page about renting the house whole, contact, and the two legal pages. A thirteenth route is the admin login. Every heading, paragraph, image and button on those pages is one of 151 editable slots, and each one ships with its original text beside it in the source, so a database that cannot be reached leaves the page complete rather than blank.

    A gallery of nine interior photographs in a three-column grid on a laptop, and the same section on a phone as one full-width photograph per screen.
    The interior set on the gallery page, one of three that hold 75 images between them. On a phone the three-column grid becomes one photograph per screen. The exterior set was kept out of frame, because one of its images carries a vehicle registration plate.
  2. 02

    One calendar out of three sources

    The availability endpoint reads the Airbnb feed, the Booking.com feed and the direct bookings in the database, merges them into one set of blocked nights, and remembers which source blocked each one. A feed having a bad afternoon is skipped rather than fatal, because a booking platform being down should not take the website with it. The merged answer is cached for sixty seconds, and every write collapses that window to nothing.

    A booking dialog showing September and October 2026 side by side on a laptop and stacked on a phone, with several Friday-to-Sunday runs struck through in grey and a legend reading check-in, check-out, unavailable.
    The booking calendar as a guest meets it, two months at a time, with the unavailable nights struck through. Every struck-through run here was produced by the app’s own three-way merge running over invented calendars: both platform feeds and the direct bookings were replaced with fixtures and nothing after them was touched. The guest is not told which channel took which night, and neither is the owner on this screen. The per-source colours this component contains cannot render in the shipped build, because the dialog is mounted outside the provider that would tell it an owner is looking.
  3. 03

    The form re-reads all three at the moment it writes

    The booking form does not trust the calendar it has just drawn. On submit the server runs eight validations against the rules held in the settings document, minimum and maximum nights, how far ahead a stay may be booked, party size, turnaround between guests, and then refetches both platform feeds uncached, because the only reading that counts is the one taken at the moment of writing. Then it writes the reservation, emails the guest and the owners, and busts the availability cache.

    A booking form with an empty name field, empty phone and email fields, a guest count of two and an empty message box, above a back button and a send-request button.
    The second step, left empty. Every string in the fields is the app’s own placeholder text rather than typed input: no name, number or address was entered at any point and the form was never submitted. The summary bar keeps the two nights chosen and the total the dialog worked out from the property’s own published nightly rate. The line under the buttons states what the owners are promising, which is that the request is not confirmed automatically and the dates are held until they call back.
  4. 04

    And it publishes in the other direction

    Direct bookings go back out as a calendar feed the two platforms subscribe to, so a weekend sold over the telephone closes on Airbnb and on Booking.com without anyone opening either. One detail from the build is worth keeping. Airbnb rejects a feed with no events in it, so the feed carries a single all-day event on 1 January 2024 titled Calendar sync active. It is emitted unconditionally, and it is the reason the integration survives a quiet month.

  5. 05

    An assistant that is not allowed to guess about dates

    A guest asks in Romanian and it answers out of a knowledge base built from the site’s own pages. Dates are the exception. The prompt requires it to call the availability check immediately and to answer exclusively from what comes back, never from memory and never from the page. The check calls the same endpoint the booking form calls. If the check fails, what the assistant is handed is the telephone number rather than a guess.

    A chat panel holding two exchanges: 18 to 20 September answered as unavailable with the occupied nights named, and 23 to 25 October answered as free. On the phone the panel takes the whole screen.
    Asked whether 18 to 20 September is free, the assistant does not answer from memory. The server fires the availability check, and the reply reads back what came out of it: not free, taken on the 18th and the 19th. 23 to 25 October comes back free. Both of those sentences were computed by the app’s own merge over the invented calendars, which is why they name exactly the nights the fixtures block. The wording wrapped around them was written for the capture, because this copy of the build has no model key and no model was called.
  6. 06

    It moves the page rather than describing it

    Six tools sit behind the conversation and the browser is what executes them. It opens a page, scrolls to a section, checks dates against the merge, offers the booking, and opens the booking dialog. So the answer about what the house has arrives with the facilities page already open at the right section, rather than with directions to it.

    A facilities page with a chat panel docked at the lower right: a greeting, four topic buttons, a question about what the house offers, and an answer ending with a line about opening the facilities page.
    A general question, asked on the homepage, and the browser now standing on the facilities page. The assistant returned a page-navigation call and the app’s own code dispatched it to the router, which is the part of this frame that is the shipped build doing its job. The words of the reply are scripted for the capture: there is no model key in this copy and no model was called.
  7. 07

    The owners write the site from inside the site

    Signed in, every heading, paragraph, image and button on the public pages becomes editable where it sits, 151 slots across 202 editable components, with undo and redo per field. Saving writes the content document and then regenerates the knowledge the assistant answers from, by plain string concatenation out of the same collection the page reads. There is one copy of the words, and what the assistant knows is rebuilt out of it.

    The live rooms page with a sub-heading inside a gold dashed outline and rewritten, and a dark green admin bar carrying reservations, settings, an email test, an edit toggle and sign-out.
    Editing in place, on the real page rather than in a form. The sub-heading has been clicked, which selects it whole and draws the gold dashed outline, and a new line has been typed over it. On a phone the admin bar moves to the top and wraps onto two rows, so the site can be rewritten from the same handset the bookings are answered on. Sign-in was replaced with a local flag in the copy this was photographed from and the save path was a no-op, so no account and no database were involved.
  8. 08

    The desk behind the sign-in

    Two allowlisted Google accounts reach the admin bar, and the entire admin tree is a dynamic import that loads only when the visitor looks like one of them, so the public bundle never carries it. Behind the bar sit the reservations, pending, confirmed and cancelled, the availability calendar coloured by source, the nightly rates and stay rules, and an email diagnostics panel. Cancelling a booking leaves the row and reopens the nights; only an already cancelled booking can be deleted.

    A reservations panel with tabs counting pending, confirmed, cancelled and total, a two-month availability calendar colour-coded red, blue and green with an Airbnb, Booking and direct legend, and a booking card beneath it.
    The owners’ desk, on a laptop and on the phone it will actually be used on. The calendar at the top is the three calendars merged and coloured by where each night was sold, red for Airbnb, blue for Booking.com, green for a booking taken by telephone, and it is the only place in the shipped build where those colours render. Under it are the pending requests, each with a number and a mailbox one tap away. Every booking, name, number, address and message in these two frames was invented for the capture, and the feed address printed at the foot of the panel was changed to an example domain.
  9. 09

    The same booking, in three places

    A weekend sold on the telephone is typed in once. It shows up as three green nights in the owners’ calendar, as a card on the confirmed tab, and as the reason the assistant tells the next guest those nights are gone. It leaves by the outbound feed at the same time and closes the same weekend on both platforms. One write, four surfaces, and no second copy of the truth to fall out of step.

    The reservations panel on the confirmed tab, showing a booking for 18 to 21 September carrying a confirmed badge, above the start of a second confirmed booking.
    The confirmed tab, and the loop closing. The stay dated 18 to 21 September is the booking the calendar above it paints green on three nights, and the same one the assistant reported as taken two frames ago. Invented booking, invented person, one merge underneath all three screens.

The scheduled sync was deleted sixteen minutes after it was written, because the plan that runs it costs money. What replaced it refreshes the calendar on demand, at most sixty seconds stale, thirty times fresher than the schedule would ever have been.

The whole editing loop on a phone in seven seconds. Edit mode on, the sub-heading tapped so it selects itself whole, a new line typed over it, then Escape, which puts the original sentence straight back. Sign-in was replaced with a local flag in the copy this was filmed from and the save path was a no-op, so nothing was written to any database.

The parts worth telling

Two characters that made Airbnb reject the feed

The timestamp helper already ended in a Z. The first version of the outbound feed stripped the milliseconds off it and then appended a second one, and that one-line typo invalidated the entire outbound integration, which is the half of the system that stops a weekend sold on the telephone being sold again on Airbnb. Nothing threw. The only symptom was Airbnb declining to subscribe. The commit that fixed it, on 2 April, takes out a single call.

A fetch that succeeds is not an email that arrived

Reservation emails failed silently in production for months, and two faults were stacked. A non-2xx from the mail provider is a perfectly successful fetch, so every rejection for an unverified sending domain read as a send. And the promise doing the sending was floating, so the serverless invocation was killed the moment the response went out and the call never settled, which lost even the error. One afternoon in August repaired both and added a panel that puts a failure on screen instead of leaving it in the logs.

What the assistant is allowed to spend

Every call is priced from its own token usage against a ceiling of ten dollars a day. The ceiling is real and it is softer than it looks: the counter it reads is a variable inside the serverless instance, so a cold start puts it back to zero, and the authoritative total written to the database is read by nothing and displayed nowhere. A one-token health check confirms the key still works, and when it does not, the chat bar is not rendered at all. A guest never meets a chat that is broken.

The codebase already knew this bug by name

In April a commit fixed a timezone fault that had been shifting every blocked date a day late in the two calendars where it had been seen: dates built in local time and then formatted as UTC. The same mixture is still in the server’s turnaround loop, where it drops a day on 28 and 29 March in Romanian time. It is latent, because the turnaround buffer defaults to zero. There is no test in this repository that could have found the third place.

How it works

  • The Airbnb feed
  • The Booking.com feed
  • Direct bookings, taken by telephone
One merged set of blocked nights, at most sixty seconds old
  • The booking calendar
  • The assistant’s availability check
  • A feed the two platforms subscribe to

Numbers

8
rooms, one house, rented whole, at 1290 m
3 → 1
calendars merged, and one published back out
60 s
the longest the merged calendar can be stale, where a cron would have left 30 minutes
79
commits over nine working days, 24 March to 17 August 2026, 39 of them on 2 April

Built with

  • Next.js 16
  • React 19
  • TypeScript
  • Tailwind 4
  • Firestore
  • Firebase Auth
  • Firebase Admin
  • iCal, in and out
  • Claude Sonnet 4.6
  • Claude Haiku 4.5
  • Resend
  • Google Places
  • Framer Motion
  • Lenis
  • Vercel

Live, and a client’s business, in Romanian throughout. Four things are honestly unfinished. There are no tests at all, no runner and no test file, so every rule in the booking flow is upheld by reading the code. The linter reports two errors and thirty-one warnings. The booking dialog still carries per-source colours it can never draw, for the reason given under the calendar above. And the route the deleted cron used to call is still there, still commenting that a schedule calls it every thirty minutes, called by nothing.