Alpenglow Retreat
Booking site and staff operations dashboard on one database
Role: Full-stack: schema, RLS, RPCs and UI

- Postgres functions
- 11
- cabins, publicly readable
- 15
- rows anon can read from bookings
- 0
- lines across 96 source files
- ~11.7k
Overview
A public marketing website and a staff operations dashboard for a fifteen-cabin retreat, sharing one Postgres database, one design language and one deploy.
The public site carries a full-bleed hero, the estate's story, fifteen rooms with client-side filtering and sorting, a season guide, and an enquiry form whose limits are read live from the hotel's own settings row: change the minimum stay in the dashboard and the public form changes with it.
The dashboard runs the business: a triage inbox for enquiries, server-paginated bookings with every filter in the URL, check-in and check-out, cabins with photo upload, team management and settings. Four Postgres RPCs do the dashboard aggregation, so the client does no maths.
The screens
15Cabin listing with filtering by guests, nights and features
Captured from the running system. Records shown are demonstration data.
The interesting part
The browser only ever holds an anon key.
Almost no authorization lives in the React. Row-level security, column grants and database functions decide what anyone can read or write. Probed live against the REST API with the anon key, an anonymous visitor sees 15 published cabins and one settings row, and zero rows in bookings, guests, staff and reservation requests. The enquiry insert deliberately does not chain `.select()`, because anon has insert-but-not-read on that table and asking for the row back would be rejected. Converting an enquiry into a booking is a single transaction inside `hotel_convert_request`.
What it does
- Public site: home, rooms index with six filter chips and four sorts, room detail, the estate, contact
- Enquiry pipeline from anonymous insert through staff triage to a real booking
- Dashboard KPIs over 7, 30 or 90 days with a gap-filled sales series and stay-length donut
- Server-paginated bookings with whitelisted sorts and debounced guest search, all URL-driven
- Check-in with breakfast pricing and an explicit payment attestation
- Cabin CRUD with photo upload that rolls back if the row insert fails
- Team management behind an edge function holding the service role
- Three-way light / dark / system theming that stays live with the OS
- Build-time sitemap generated from the live cabins table
- WCAG 2.1 AA: 12 axe violation groups across 173 elements, taken to zero
Built with
- Framework
- React
- Vite
- React Router
- UI
- Styled Components
- Recharts
- React Toastify
- Data
- Supabase
- PostgreSQL
- TanStack Query
- Forms
- React Hook Form
- Backend
- Row Level Security
- 11 Postgres functions
- Edge Functions
- Storage
- Tooling
- Image pipeline (sharp)
- Sitemap generator
Next project
Moonlight Store
E-commerce client over a REST API