All projects

Case study · Hospitality · AI platform · Jun 2026 – present

HotelClaw.ai.

An assist-only AI platform for boutique hotels: guest and staff messages captured and routed by an LLM, grounded in the hotel's own data, with staff sending every reply. I own the PMS integration layer.

PrivateFull-Stack & AI Engineer (contract) · platform & security

5

PMS adapters behind one sync runner

39

fixture tests across the PMS adapters

54 → 0

exceptions in the tenant-isolation check

~600

merged PRs since June 2026

The problem

A boutique hotel's guest conversations are spread across WhatsApp, LINE, email and the staff group chat, and nothing joins them to the stay they belong to. The stay itself lives in the property management system (PMS), which changes from hotel to hotel: Oracle OPERA at one, Mews or Cloudbeds at the next.

An assistant that drafts replies has to know who the guest is and what the hotel's procedures say, and it has to be safe to run for many hotels on one database. Staff also have to trust it before they use it, which rules out an AI that answers guests on its own.

What I built

One PMS layer for five systems

The pilot resort runs Oracle OPERA and sends report exports, so the first piece was a translator for those exports, built for the imperfect files a real front desk produces. Re-importing the same export changes nothing. I then built an Opera Cloud (OHIP) adapter, generalised its sync into a single runner that any PMS plugs into, and added adapters for Mews, Cloudbeds and Apaleo on top.

  1. OPERA export or PMS APIReport files today; Opera Cloud, Mews, Cloudbeds, Apaleo next
  2. AdapterOne per PMS, each with fixture tests
  3. One sync runnerShared by every adapter; safe to re-run
  4. Stays in PostgresUnder the hotel's organisation id

OPERA exports are what runs today. The four API adapters are covered by 39 fixture tests, and Cloudbeds and Apaleo were built from the vendors' published API documentation, so each one gets proven against its first real hotel rather than assumed to work.

Messages in, with proof of where they came from

LINE and WhatsApp deliver guest messages by webhook. I wrote the edge function that receives them: it checks each request's HMAC signature with a constant-time comparison before anything is stored, then writes the message to the event log, the table I set up as the platform's record of what happened. Live traffic on both channels is waiting on the providers' business verification.

An intent router that leaves the reply to staff

Each guest message then goes through an LLM intent router, most of which I wrote. It works out what the guest wants and which workflow handles it. High-risk intents are always sent to a person, and AI drafts are held as pending until a member of staff approves them. Models are called through OpenRouter, so the provider can change without a rewrite.

  1. Guest messageLINE or WhatsApp webhook
  2. Signature checkHMAC, compared in constant time
  3. Event logThe record of what happened
  4. Intent routerIntent, workflow and a grounded draft
  5. Staff replyDrafts wait for approval; high risk goes to a person

On a 50-message test set in June the router picked the right intent 90% of the time, and an acceptable one 98% of the time. A 50-message replay in October scored 80%, with its labels still in draft, so that number means little until the labelling settles.

Grounded in the hotel's own knowledge

Drafts draw on the hotel's procedures and the guest's history through RAG on Postgres with pgvector. I created the tables for message embeddings, knowledge-base documents and the knowledge graph, all behind the same row-level security as everything else.

Tenant isolation as a test, not a hope

Every table carries the hotel's organisation id, so adding a hotel is a new row, not a code change. That only holds if no query can cross from one hotel to another, so I made it a check that runs in CI: it fails if any table can be read across organisations or without signing in. Row-level security is on for every table, and the check's list of tolerated exceptions went from 54 to zero. Along the way it surfaced cross-tenant and anonymous read paths, and I closed them.

The baseline

On my first day I set up the engineering baseline the repository runs on: Biome for formatting and lint, git hooks, pull-request gates, CI on every change and secret scanning with gitleaks. Since June I have merged about 600 pull requests.

5

PMS adapters, one live and four fixture-tested

39

fixture tests across the adapters

0

exceptions left in the isolation check

90%

router top-1 on a 50-message set (June)

Trade-offs

  • Assist-only, not autonomous. Letting the AI answer would be faster. Keeping staff on every reply leaves the hotel accountable for what its guests are told, and lets staff learn to trust the drafts before they lean on them.
  • Adapters ahead of the hotels. Four adapters exist before a hotel needs them, so onboarding becomes configuration rather than a project. The cost is code that is fixture-tested but unproven until it meets a real account.
  • Isolation in the database. Row-level security puts the tenant boundary in Postgres, where a missing filter in application code cannot cross it. The price is a policy on every table, and a check to keep it there.
  • One-way master data. Hotel master data flows from Google Sheets to Notion to n8n to Supabase and is never written straight into the database, so staff edit in tools they already know and the platform has one source of truth.

Stack

Oracle OPERA
Opera Cloud (OHIP)
Mews
Cloudbeds
Apaleo
Supabase
PostgreSQL
pgvector
Row-level security
Clerk
OpenRouter
n8n
WhatsApp
LINE
Next.js 16
Expo
Biome
Gitleaks

Want help shipping something like this? Book a call, or grab the snippets this case study draws from.