Case study / 01

Hotel Operations Platform

React / TypeScript / Node / SQL

A production operations system connecting room status, housekeeping, handovers, maintenance and asset workflows across three Sydney hotels.

← All projects
System architecture01 — Operations
01Staff workspaceReact / TypeScript
02Rules & permissionsNode / Scoped services
03Operational recordSQL / Audit history

Property isolation at the service boundary

RoleRequirements, workflow design and AI-enabled delivery
ContextInternal platform used across three operating hotels
StatusProduction / private

Challenge

Replace fragmented paper, spreadsheet and chat-based workflows without interrupting live hotel operations.

Contribution

Led requirements, workflow design and delivery from the operational floor. Built the platform through an AI-enabled development workflow, reviewing business rules, access boundaries, tests and production behaviour.

  • 104Database tables
  • 594REST endpoints
  • 3,434Test cases
  • 60 × 7Permission keys × job roles
  • 3Live properties
  • 1Developer — requirements to operations

All figures counted from the codebase, September 2026.

The problem

Housekeeping ran on paper,
spreadsheets and a chat group.

A daily clean sheet, a weekly deep-clean sheet, a chat group for room-status calls, a spreadsheet for piece-rate wages and Microsoft Lists for the handover log. Nothing was queryable, nobody could prove a room had been inspected, and a newly added room type silently paid a housekeeper nothing.

The platform replaced all of it for a three-property boutique hotel group: room status, cleaning and inspection dispatch, maintenance escalation, public-area rotas, shift handover, key and lost-property ledgers — and the modules that hang off them: piece-rate payroll, points and rewards, e-signed onboarding, inventory and an owner-facing revenue digest. Every property's data is scoped separately; every screen and endpoint is gated by a 60-permission matrix across seven job roles.

Seven roles, seven surfaces

Each job got its own screen,
not a filtered version of one.

Permissions live in a single code registry, sync into the database on boot and can be re-shaped by an admin at runtime — so the business can change a role without a code change.

01 Mobile

Housekeeper

  • Task list on their own phone, tabbed by work kind with large tap targets
  • Start → complete → photo evidence; minimum-photo policy enforced server-side
  • Do-Not-Disturb and no-service as first-class outcomes; locked work is explained, not hidden
  • Own piece-rate income, points, rewards shop and documents to sign
02 Mobile

Houseman

  • Special-task queue: deliveries, rubbish runs, deep-clean items
  • Maintenance jobs they can complete themselves
  • Two-way conversion between maintenance tickets and houseman tasks
  • Inventory counting and stock movement from the phone
03 Mobile

Inspector

  • Queue of finished rooms with three explicit outcomes: Ready, Vacant Clean, rework
  • Rework is a round on the original task, so history stays one thread and pay is not duplicated
  • Scored, itemised room audits with a printable report
  • Inspector authority can be granted by duty roster, not only by role
04 Desktop + mobile

Reception

  • Live room board with batch transitions and guardrails on the high-risk ones
  • One merged dispatch entry point; report-triage and express-checkout queues
  • Shift handover board with follow-ups, confirmation and Excel import of the legacy sheet
  • Key audit ledger, lost property, night-audit reconciliation, day-use handling, contractor kiosk
05 Desktop

Manager

  • Everything reception has, plus authority to approve, assign and escalate
  • Piece-rate payroll view and daily task reconciliation — the wage basis, so staff cannot adjust their own
  • Approval inbox scoped to their own property
  • Public-area checklists, rosters, inspector rotation, announcements
06 Desktop

IT operations

  • Account, property and room configuration
  • PMS integration health: webhooks, sync runs, watermarks, anomalies
  • Notification switchboard and wiki authoring
  • Deliberately withheld: payroll, the irreversible “paid” step, staff identity documents
07 Desktop

Owner

  • Access-control matrix: reassign any permission to any role at runtime
  • Scheduled revenue digest; team day report; public-area weekly overview
  • Staff document vault with e-signature, PDF field annotator and form builder
  • Staff-facing changelog: 461 entries written for the person reading it

Architecture

One write path per concept.
All SQL in one layer.

Every mutating endpoint passes three stages: authenticate, permission key, then row-level property scope asserted on the fetched row itself.

  1. 01
    Desktop SPA · 79 routes / Mobile shell · 30 routesReact 18 · TypeScript · Vite · Ant Design tokens · Zustand · TanStack Query · react-intl (zh / en)
  2. 02
    Express API · 61 route modules · 594 endpointsauthenticate (JWT) → requirePermission(key) → enforceHotelAccess(row.hotel_id)
  3. 03
    49 services · 8 schedulers · audit loggerBusiness rules, money invariants that throw, append-only ledgers
  4. 04
    45 repositories — the only SQL in the systemServices compose repositories; routes compose services; no route builds a query
  5. 05
    SQLite (better-sqlite3, WAL) · 104 tables150+ ordered idempotent migrations, integrity-checked, pre-migration snapshots
Property management system (external)
① read-only mirror② projection③ gated write-back
  • Nginx · PM2 · nightly backup cron
  • Atomic frontend swap: build to staging, then move
  • Also containerised: multi-stage build, non-root, healthcheck

The hardest part

A transition is an event,
not a snapshot.

The PMS owns reservations and room condition, but it is not trustworthy as a driver of local work: versions can move backwards, absence is not deletion, and a static “vacant dirty” reading does not mean the room needs cleaning again.

  1. 01PMS eventOccupied → Vacant Dirty (webhook + delta poll)
  2. 02MirrorIdempotent UPSERT, version-guarded, never deletes
  3. 03ProjectionInterprets the transition; at most one checkout cycle
  4. 04HousekeepingTask dispatched, photos, locked until the guest has left
  5. 05InspectionReady · Vacant Clean · rework
  6. 06Write-backOne status, one room, three gates, per-property allow-list

Conflicts with work that is in progress, paid or manually created go to a human review queue instead of being overwritten. Repairing or replaying source data can never rewrite operational work.

Database design

Decisions
worth reading.

104 tables in SQLite, reached only through 45 repository modules. Every business table carries hotel_id; multi-tenancy is a column enforced by one helper.

01

Money is an integer number of cents

Payroll amounts were floats and a week's wage bill was a float SUM. Integer cent columns were added and backfilled; every sum runs in cents and dollars appear only at the edge.

02

Prices are snapshotted at completion

A completed task stores the rate that applied at that moment. Three rate classes replaced a rate-per-room-type table that let a new room type silently pay nothing.

03

Money invariants throw

A typed PayrollIntegrityError fails the transaction. Silently skipping is what produced “the room is done and nobody was paid”.

04

Separation of duties in the schema

Approving a payroll period and marking it paid are different keys; the paid key is seeded to no role, so granting it is always an explicit, visible act.

05

Append-only where history is the point

Audit logs, assignment history, room flow events, PMS transitions, write-back attempts, point entries. Nothing that matters is updated in place.

06

Time is property-local

SQLite's datetime('now') is UTC and put notifications ten hours in the past. Every write path goes through Sydney-clock helpers; tests use the same helper for “today”.

Three production stories

Judgement,
not volume.

01

The room that froze for five days

An anti-out-of-order guard refused mirror updates older than what was stored. The source system's own version number moved backwards, so one room stopped updating. Diagnosed from stored timestamps; fixed by comparing versions correctly rather than removing the guard.

02

The room type that paid nothing

Rates were keyed on the PMS's room-type string, so a new type produced zero-value payroll entries. Replaced with three rate classes, one mapping function and one lookup — no schema change, and historical money is never re-mapped.

03

Guest gone, work still locked

Checkout cleaning unlocked only at the instant the projection saw the checkout. When that instant was missed, a room sat locked for four hours on a property's first live day. A per-cycle reconciliation sweep now makes recovery independent of catching a single moment.

Engineering practices

Built to be operated,
not just shipped.

3,434 tests, invariant-focused

Service and repository tests over money and state transitions, route and permission-matrix tests, component and hook tests, property-based tests with fast-check. A failing test is never dismissed as flake.

Permissions as declared data

One registry file; boot-time sync; the same key gates the UI through <Can> and the API through requirePermission. Retiring a key is a first-class operation.

A real design system

Tokens from the client's brand, one PageFrame for every module page, one table language, at most one primary action per page, a separate mobile shell.

Release engineering, end to end

Nginx, PM2, atomic frontend swap, pre-migration snapshots, post-deploy verification, nightly backups with 14-day retention, and a staff-facing changelog with 461 entries.

Codebase

Where the ~260,000 lines of TypeScript live

Production code ~195k lines; tests ~64k lines across 340 files.

  • Backend services, repositories, routes522 files
  • Frontend components and hooks218 .tsx + 199 .ts
  • Tests340 files · 3,434 cases

What this project evidences

  • Full-stack TypeScript at scale
  • Relational modelling & ledgers
  • Schema evolution under load
  • API design & layered auth
  • Third-party integration
  • Authorization architecture
  • Mobile-first UX for field staff
  • Design systems
  • Test engineering
  • DevOps / SRE
  • Product discovery
  • Solo ownership
Full metrics table
Production TypeScript / TSX (excl. tests)~195,000 lines
Total TypeScript incl. tests~260,000 lines
Database tables104
Ordered, idempotent migrations150+ (latest deployed #156)
REST endpoints594 across 61 route modules
Repository / service modules45 / 49 (+26 PMS integration)
Background schedulers8
Permission keys / job roles60 across 8 modules / 7
Desktop / mobile routes79 / 30
Test files / cases340 / 3,434 (~64,000 lines)
Staff-facing changelog entries461
Live properties3
UI languages2 (English / Chinese)

Delivery process

How the work
came together.

From the first model of the problem to checking how the system behaves.

01

Observe the work

Mapped real front-desk, housekeeping and maintenance handoffs before deciding what the software should automate.

02

Define boundaries

Converted roles, hotel scope and exception paths into explicit access and data rules.

03

Deliver in slices

Released workflows incrementally so staff could validate behaviour while hotel operations continued.

04

Protect production

Used build, lint and test gates, audit history, deployment controls and staff documentation to reduce operational risk.

Engineering evidence

The details
that matter.

  • Per-property data isolation and role-based access
  • Audit logging and regression coverage
  • 104 tables · 594 REST endpoints · 3,434 tests (as of Sept 2026)
  • 60 permission keys across 7 job roles, reassignable at runtime
  • Bidirectional PMS integration: read-only mirror, projection, gated write-back
  • 500+ commits behind build, lint and test gates; 150+ idempotent migrations
  • Production on Linux with PM2 and Nginx, nightly backups, atomic deploys

Inside the decisions / Expand to read

01Put property boundaries in the service layer

A hotel selector is useful for navigation, but each request also needs to respect the user's permitted property. Keeping the rule at the service boundary makes it apply when an operation reaches the API outside the expected interface flow.

02Keep operational exceptions visible

Room status, housekeeping and handovers have different owners. Explicit states let staff see unfinished work and exceptions, and give the software concrete transitions to validate.

03Pair assisted implementation with review

AI tools help produce and iterate on changes. I own the requirements, business rules, integration, debugging, test review and acceptance of the result in the working environment.

04Make changes traceable

Audit history supports investigation after a change; regression tests protect known behaviour before release. Both matter when several properties depend on the same system.

Outcome

The platform moved from an individual build into live multi-property operations, replacing disconnected processes with one auditable system.

AI-enabled delivery: I own the requirements, business rules, review, integration, testing and operational rollout. This public case study uses architecture descriptions; production data and source remain private.

  • React
  • TypeScript
  • Node.js
  • Express
  • SQL
  • Linux
  • PM2
  • Nginx

Next case study

02Power Platform Internal Operations Suite→