XState-driven PMS for real property deployment
Hotel Elegent is a full-stack hotel property management system built for a real deployment — a family hotel in Sawai Madhopur — and simultaneously packaged as a white-label ThemeForest product. It covers the full hotel operations domain: a multi-step booking engine, front desk calendar, restaurant POS, housekeeping and maintenance tracking, guest CRM with loyalty points, digital self-service check-in, concierge and guest request management, revenue analytics, and a dynamic pricing rules engine. The entire system is driven by a 46-schema PostgreSQL database, configured through an 8-step installation wizard, and deployable to a VPS with a single shell script.
Commercial hotel PMS products charge ₹5,000–20,000 per month with no self-hosting option. This replaces that recurring cost with a one-time deployment on owned infrastructure, while being generic enough — white-label currency, swappable payment gateways, dual image hosting — to sell globally on ThemeForest. The same codebase serves the uncle's property in production and the ThemeForest submission ZIP.
The project ran for 16 months across 23 named sprints and 195 commits, reaching 111/111 Playwright E2E tests before the ThemeForest package was assembled.
A 7-step booking flow involving real-time availability, guest authentication, payment, and confirmation has at least 15 distinct failure modes — payment retry, mid-flow abandonment, availability changes between steps, step navigation backward and forward. Managing this with nested useState and conditional renders would scatter the failure logic invisibly across components. The state had to be made explicit and testable, which meant choosing a tool designed for that job rather than improvising with React primitives.
The white-label requirement added a second layer of constraint: every piece of hardcoded configuration — currency symbols, image hosting provider, payment gateway keys, branding — had to be extractable to settings without touching source. Replacing 100+ hardcoded ₹ and INR references with a dynamic currency formatter mid-project (Sprint 14) is the kind of work that distinguishes a "hotel template for India" from a product a hotel in Dubai can actually use.
The stack is Next.js 15 (App Router, React 19, Turbopack) running on Bun with strict TypeScript throughout. State management follows a strict three-system rule enforced in CLAUDE.md: XState v5 owns the booking flow FSM (bookingMachine.ts, 280 lines — 7 states, explicit guards, error/retry, RESET and BOOK_MORE transitions); Zustand v5 owns client-side form state collected during the flow; TanStack Query v5 owns all server data fetching. The three systems do not cross boundaries — no server data in Zustand, no form state in TanStack Query, no flow logic in either.
The 46 Drizzle schema files cover the complete hotel domain: rooms, bookings, folio charges, payments, housekeeping, maintenance, restaurant tables, orders, menu, pricing rules, coupons, occupancy snapshots, loyalty points, reviews, guest profiles, notes, messages, notifications, WhatsApp, audit logs, digital check-in tokens, concierge, channel manager, shift notes, staff profiles, site settings, installation config, invoices, and 2FA. Primary key convention is deliberate: UUIDs for content tables (stable, URL-safe, non-leaking) and serial integers for transactional rows (bookings, payments) where monotonic ordering benefits index efficiency and audit trails. Real-time room availability, concierge requests, and restaurant order updates flow through Soketi (self-hosted) or Pusher (production) on the Pusher protocol. Payments support Razorpay (India), Stripe (international), and PayPal, all read from environment variables — swappable without code changes.
XState v5 for the booking FSM. The 7-step flow has too many failure modes to manage safely with imperative state. An FSM makes illegal transitions impossible by construction — jumping from idle to payment without passing through roomSelection and guestDetails simply cannot happen. Each state is independently addressable, which is what made 111 E2E tests achievable without complex setup scaffolding.
Three-state-system rule (XState / Zustand / TanStack Query — no mixing). Each library owns a specific concern and the CLAUDE.md prohibits crossing those boundaries. This prevents the common anti-pattern of storing server data in Zustand or putting form state in TanStack Query — both of which produce subtle staleness and hydration bugs that are hard to bisect.
UUID vs serial PK discipline. Content tables use UUIDs for stable, non-sequential IDs safe for URLs and external references. Transactional tables use serial integers for index efficiency and natural audit ordering. The split is encoded in the Drizzle schema from the first sprint, so it never had to be retrofitted.
White-label currency as a product decision. Replacing 100+ hardcoded ₹ / INR strings with a dynamic currency formatter is what separates a regional template from a globally sellable ThemeForest product. A buyer in Dubai or London should not have to grep-replace currency symbols — it comes from the settings panel.
setup-vps.sh as a first-class deliverable. ThemeForest buyers abandon products that require manual Nginx config and SSL setup. The one-command provisioner handles Docker install, Certbot SSL, Nginx HTTPS reverse proxy (443→3000), WSS proxy for Soketi, backup cron, and renewal cron — everything a non-technical hotel owner needs to go from a blank VPS to a running system.
Playwright E2E tests passing
Drizzle schema files
commits across 23 sprints
lines of TypeScript
Did this resonate?
Hotel Elegent is production-deployed to the family hotel in Sawai Madhopur, eliminating a recurring commercial PMS cost of ₹5,000–20,000 per month. The ThemeForest submission package — 134,000 lines of TypeScript, 195 commits, 23 sprints, 46 schema files, 111/111 Playwright E2E tests — is complete. Three payment gateways, dual image hosting (Cloudinary or self-hosted Openinary), a white-label currency system, and a one-command VPS provisioner make it deployable by a non-technical buyer without a DevOps hire.