Stays for the quietly curious.
A boutique-hotel booking platform for India — search, book, host, review.
Editorial design system. Light + dark mode. Open source.
Setup · Features · API · Architecture · Deploy
- Why Nestoria
- What it does at a glance
- Tech stack
- Requirements
- Install
- Option 1 — Local development (full hand-holding in docs/SETUP.md)
- Option 2 — Deploy to Render (full walkthrough in docs/DEPLOYMENT.md)
- Configuration
- Features in depth
- Project layout
- Storage & settings
- Privacy & data
- Scripts
- Troubleshooting
- Roadmap
- Contributing
- Credits
- License
Most hotel booking apps look — and feel — like booking apps. Loud price banners, "8 people are looking at this hotel right now", tabs full of upsells. They're optimised for transactional volume, not for the slow, considered way people actually plan trips.
Nestoria is the opposite. It's a small, opinionated platform for independent Indian stays — restored havelis, beach houses, plantation bungalows, desert guesthouses — presented like a travel journal rather than a marketplace.
Built around three principles:
- Editorial first — every screen feels like a magazine spread. Long line lengths, serif display type (Instrument Serif), monospace for data (JetBrains Mono), generous white space, and a warm cream/ink palette that works in both light and dark mode.
- Open and self-hostable — every line of code is here. The full stack runs on a free tier (Render + Supabase). You can fork it and run your own collection.
- Honest data model — Hosts own properties; properties have rooms; customers book rooms; reviews update ratings via Bayesian-smoothed triggers. No marketplace fees, no surge pricing, no fake urgency.
- Browse 8+ curated stays across India — Udaipur, Goa, Coorg, Munnar, Jaisalmer, Auroville
- Search and filter by location, price stepper, rating, region, or amenity — with a type-to-suggest Where popover and a real date-range calendar
- Read rich hotel detail pages with 5 tabs: Overview, Amenities, Rooms, Reviews, Location (live Leaflet map at the real lat/lng)
- Save hotels to a server-side favourites list that survives device switches and works for customers and hosts
- Book in a 3-step wizard with a simulated payment terminal (card / UPI / pay-at-hotel) and a per-reservation detail page at
/reservations/:id - Review completed stays — separate ratings for the hotel and the room, with auto-recomputed averages
- Sign in with email + password (bcrypt) or Google (Google Identity Services)
- Host workspace — 5-tab dashboard (Overview, Properties, Bookings, Earnings, Profile) with KPI cards, revenue chart, and a filterable bookings table; onboarding banner gates listing until the host's business profile is complete
- List a property via a 3-step wizard — basics, address + amenities + interactive Leaflet map pinpoint, rooms (with name, type, special amenities, and image upload to Supabase Storage); publish disabled until at least one room exists
- Switch themes — light / dark with FOUC-free initial render, plus 5 swappable accent palettes (Terracotta, Forest, Ink, Saffron, Plum)
| Layer | Technology | Why |
|---|---|---|
| Database | PostgreSQL 14+ | Robust, free, supports the rating triggers cleanly |
| Backend | Node 18+ · Express 4 | Tiny, predictable, no magic |
| Auth | bcryptjs + jsonwebtoken + google-auth-library |
Email/password + Google ID token verification, no third-party SDKs |
| Object storage | Supabase Storage | Free tier, S3-compatible, RLS-friendly |
| Frontend | React 18 + Vite 5 | Sub-second HMR, zero server runtime |
| Routing | react-router-dom v6 |
Industry standard |
| Data layer | @tanstack/react-query |
Caching, dedup, optimistic updates |
| Forms | react-hook-form + zod |
Type-safe forms, zero re-render cost |
| Maps | leaflet + OpenStreetMap tiles |
No API key, vector-quality, ~40 KB gzipped |
| Styling | Hand-rolled CSS with OKLCH variables | Editorial design needs hand-tuned colour, not utility classes |
| Hosting | Render (web + static + Postgres) | Free tier, blueprint-driven |
See docs/ARCHITECTURE.md for the longer "why" behind each choice.
| what | how to check | |
|---|---|---|
| Node.js 20+ | runtime for backend + Vite | node --version |
| npm | comes with Node | npm --version |
| PostgreSQL 14+ | the database | psql --version |
| Git | to clone the repo | git --version |
| A modern browser | Chrome 110+, Safari 16+, Firefox 110+ — the design uses oklch() and color-mix() |
— |
Anything that prints "command not found" is something you need to install. The hand-held walkthrough is in docs/SETUP.md and covers every prerequisite from scratch, including the Postgres install paths discussed below.
⚠️ pgAdminalone is not a Postgres server — it's only a GUI client. You still need an actual Postgres server. See the Postgres install paths below.
Optional but recommended:
- Supabase project with a public
hotel-imagesbucket — required only for image uploads - Google OAuth Client ID — required only for Google sign-in
For hacking on the code with hot reload on the frontend and nodemon on the backend. The full walkthrough — including how to install every prerequisite from a blank Mac — is in docs/SETUP.md. The TL;DR:
Pick one. Detailed instructions for each are in docs/SETUP.md → Step 2.
| Path | Best for | Install |
|---|---|---|
| Postgres.app ⭐ | Mac users who want a GUI menu-bar app | Drag to /Applications, click Initialize |
| Homebrew | Mac users who already use Homebrew | brew install postgresql@16 && brew services start postgresql@16 |
| Docker | Anyone who already uses Docker | docker run -d --name nestoria-postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=nestoria_db -p 5432:5432 postgres:16-alpine |
| Cloud (Supabase / Neon / Render) | No local install at all | Sign up, copy the connection string into backend/.env, set DB_SSL=true |
Already have pgAdmin but no server? pgAdmin is just the GUI client — you still need one of the four paths above. The good news: pgAdmin ships a bundled psql at /Applications/pgAdmin 4.app/Contents/SharedSupport/psql you can add to PATH if you don't want a second one, and you can use pgAdmin's GUI Query Tool to load the schema files. Step-by-step pgAdmin instructions are in docs/SETUP.md → Method 2.
# 1. Clone
git clone https://github.com/Avaneesh40585/Nestoria.git
cd Nestoria
# 2. Create + seed the database
createdb nestoria_db
psql nestoria_db -f database/001_schema.sql
psql nestoria_db -f database/002_triggers.sql
psql nestoria_db -f database/004_seed.sql # optional sample data
# 3. Backend
cd backend
cp .env.example .env
# Edit .env: DB_USER + DB_PASSWORD to match your Postgres, JWT_SECRET=$(openssl rand -base64 48)
npm install
npm run dev
# → http://localhost:5000/api/health
# 4. Frontend (in a new terminal)
cd ../frontend
cp .env.example .env
npm install
npm run dev
# → http://localhost:5173Every account loaded by 004_seed.sql uses the password password123 (bcrypt cost 10). Toggle "I'm travelling" on the sign-in screen for customer accounts and "I'm hosting" for host accounts — the role determines which table is queried, so the wrong toggle returns "Invalid credentials" even with the right email.
| Role | Password | |
|---|---|---|
| Host | vikram@marigold.in |
password123 |
| Host | priya@casapamparo.in |
password123 |
| Host | arjun@cardamom.in |
password123 |
| Customer | customer1@nestoria.dev … customer50@nestoria.dev |
password123 |
Verify the hashes load cleanly any time with:
NODE_PATH=backend/node_modules node scripts/check-seed-passwords.js
# expect: 53 pass · 0 fail · 0 skipped (Google-only)💡 Don't want sample data? Skip step
004_seed.sql. You can register fresh accounts through the UI.💡
psql: command not found? Postgres.app and Homebrew putpsqlsomewhere your shell doesn't search. Add it toPATH— see docs/SETUP.md → Step 3.💡 Hate the CLI? docs/SETUP.md → Method 2 shows how to load the schema entirely through pgAdmin's GUI Query Tool.
Render's free tier hosts the backend, frontend, and Postgres in one click via render.yaml.
git clone https://github.com/Avaneesh40585/Nestoria.git
cd Nestoria
# Push to your own GitHub repoThen follow the detailed walkthrough in docs/DEPLOYMENT.md. The short version:
- Create accounts at Render and Supabase (free, no card)
- New Render Blueprint from your fork → Render reads
render.yaml - Set
SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY, andGOOGLE_CLIENT_IDin the Render dashboard - Visit
https://nestoria-frontend.onrender.com
The backend self-initialises on first boot — npm start runs backend/init-db.js, which notices the empty Postgres and loads the schema + seed (8 hotels, 50 customers, real Unsplash photos). Subsequent deploys see the populated database and skip. When you're ready to take real bookings, wipe the demo data with scripts/wipe-seed.js --yes — see docs/DEPLOYMENT.md → 5a.
Total time: ~10 minutes.
Both backend/.env.example and frontend/.env.example are committed. Copy each to .env and fill in real values.
| key | required | default | notes |
|---|---|---|---|
PORT |
yes | 5000 |
|
CORS_ORIGIN |
yes | http://localhost:5173 |
comma-separated for multiple origins |
DB_HOST · DB_PORT · DB_NAME · DB_USER · DB_PASSWORD |
yes | — | PostgreSQL connection |
DB_SSL |
no | false |
set true on Render or any managed Postgres requiring TLS |
JWT_SECRET |
yes | — | use openssl rand -base64 48 to generate |
JWT_EXPIRES_IN |
no | 7d |
any value jsonwebtoken accepts |
SUPABASE_URL |
optional | — | needed only for image uploads |
SUPABASE_SERVICE_ROLE_KEY |
optional | — | service role, NOT anon |
SUPABASE_BUCKET_NAME |
optional | hotel-images |
|
GOOGLE_CLIENT_ID |
optional | — | needed only for Google sign-in |
UNSPLASH_ACCESS_KEY |
optional | — | needed only to re-curate the demo imagery via scripts/fetch-hotel-photos.js; the running app does not call Unsplash |
| key | required | default | notes |
|---|---|---|---|
VITE_API_URL |
yes | http://localhost:5000/api |
include the /api suffix |
VITE_GOOGLE_CLIENT_ID |
optional | — | must match the backend's GOOGLE_CLIENT_ID |
The visual language is built around three typefaces and one strong accent colour:
- Instrument Serif for display headings — italicised pulls in the hero ("quietly curious")
- Geist for body text and UI chrome
- JetBrains Mono for data — prices, dates, reservation IDs, eyebrows
The palette is defined in OKLCH for perceptual uniformity and color-mix() for tinting:
- Light mode: warm cream
oklch(0.985 0.006 80)background, dark inkoklch(0.18 0.012 60)text - Dark mode: deep ink
oklch(0.14 0.008 60)background, warm cream text - Five accent presets (Terracotta default, Forest, Ink, Saffron, Plum) each with separate light/dark values
Mode is toggled via a sliding pill in the header. Accent is picked from a floating Tweaks panel (bottom-right). Both persist to localStorage and apply with no flash of unstyled content thanks to an inline script in index.html.
The home-page SearchBar is a self-contained pill with three editorial popovers (all portal-rendered above the page so they never get clipped):
- Where — text input with a type-only suggestion list pulled from
/api/hotels/destinations. Empty submit is blocked with an inlinevar(--danger)hint. - Dates — custom two-month range calendar (built on
date-fns), past days greyed out, range fill + endpoint pills in the accent colour. - Guests — popover wrapping the shared
Steppercomponent.
The /hotels page combines server-side and client-side filtering:
- Server filters:
location,min_price,max_price,min_rating,sort - Client filters: region multi-select, amenity multi-select
- Sort options: Featured (Bayesian score), Rating, Price ↑, Price ↓ — synced both ways with the
?sort=URL param, so footer "Top rated" / "Featured stays" links land on the right pill - Price range uses a min/max
Stepperpair (±₹500), no more chip presets
The home page also surfaces five popular city chips (Udaipur, Goa, Coorg, Munnar, Auroville) that pre-fill the search bar.
Each hotel has its own /hotel/:slug route with a tabbed body and a sticky booking sidebar:
- Overview — long-form description plus a four-up Quick Facts row (check-in/out times, rooms, host)
- Amenities — grid of every amenity with the matching icon
- Rooms — every room listed with type, view, beds, size, price, and a Reserve button that deep-links into the booking wizard
- Reviews — the latest 20 reviews with avatar initials and dates; aggregate rating up top
- Location — Leaflet map with an OSM tile layer, centred on the property's real
latitude/longitude(seeded indatabase/007_hotel_coords.sql); "Open in OpenStreetMap" link for full-page view
The sidebar mirrors the design's book-card: live nights × rooms calculation, 18% GST line, and total. Picking a room scrolls the sidebar's reserve action to that room's price. The Save heart and Reserve button route anonymous users to /login?next=<current-path> and bounce them back after sign-in; host-role users get a friendly "Hosts can't book on Nestoria" message instead of a silent failure.
- Guest details — name, email, phone, arrival time, special notes (pre-filled from the logged-in user)
- Payment — Card / UPI / Pay-at-hotel chips. Card + expiry + CVV inputs are masked and digit-formatted. Confirming Card or UPI opens a portal-mounted payment terminal overlay that walks through
Authorising → Contacting bank → Approved(UPI variant:Generating collect request → Awaiting approval → Confirmed) before triggering the actual booking mutation. Pay-at-hotel skips the terminal. Integrating a real PSP is still on the roadmap — the simulation gives the editorial feel without one. - Confirmation — checkmark, reservation reference (
NSTRA-000123), one-click "View reservation" →/reservations/:idor back to home
The booking POSTs to /api/bookings, which locks the room row, checks for date overlap, and inserts atomically. Concurrent attempts to book the same room for overlapping dates return 409 Conflict.
Each booking has its own /reservations/:id page — status pill, ref code, hotel hero + room summary, dates + guests grid, property phone, and a receipt sidebar with subtotal / taxes / total / payment status. Cancel from here uses the same PUT /api/bookings/:id/cancel endpoint as the profile list. Profile's per-row View button and the booking confirmation's "View reservation" CTA both land here.
Saved hotels persist per (user_id, role) in the saved_hotels table — the heart works for both customer and host accounts, and the list survives device switches. The useSavedHotels hook drives every heart on the site through TanStack Query with optimistic updates. Anonymous clicks bounce to login with a next param so the user lands back on the page they were saving from.
Reviews are tied to completed bookings — you can't review a hotel you didn't stay at, and you can only review each booking once. Each insert/update/delete fires a Postgres trigger that recomputes:
rating_avg— straightAVG(rating)rating_count—COUNT(*)score— Bayesian-smoothed average toward the global mean (3.8), weighted with recent sentiment for a 0–100 "editorial score" used for the default sort
See docs/DATABASE.md for the full formula. The point is that one-off 5-star reviews don't catapult a brand-new hotel above an established 4.7-with-200-reviews property.
Two-track sign-in:
- Email + password — bcrypt hashes at cost 10, JWT tokens valid for 7 days (configurable),
Authorization: Bearerheader from an axios interceptor - Google Identity Services — frontend uses
@react-oauth/google'sGoogleLogincomponent; the resulting ID token is verified server-side bygoogle-auth-libraryagainst Google's JWKS
For Google sign-in, the backend either:
- finds an existing user by
google_sub, or - finds an existing email match and links Google to that account, or
- provisions a brand-new account
So a user who originally registered with email and later clicks "Sign in with Google" on the same email transparently links the two — no orphan accounts.
Customer and Host are separate Postgres tables (different columns, different lifecycles) but the backend talks to both via a generic userRepo factory in lib/userRepo.js. The result: ~80% less duplication than typical role-split backends, and the routes for /api/profile work for either role with zero special-casing.
/host/dashboard has its own 5-tab layout aimed at property owners:
- Overview — Revenue chart (per-month bars, animated), occupancy progress bars per property, upcoming arrivals table
- Properties — every hotel the host owns, with per-property occupancy, MTD revenue, and rating; one-click into the AddRooms wizard or the public detail page
- Bookings — full bookings table across all properties, with status pills and reservation IDs
- Earnings — YTD total, next payout estimate, last 30 transactions
- Profile — business name, GST number, payout account, Superhost badge
KPIs (revenue, bookings, occupancy, rating) are all computed server-side in one CTE per endpoint, so the dashboard never sends N+1 queries.
/host/add-rooms walks hosts through listing a property:
- Basics — name, URL slug, region, city, description, mood/colour (now a working swatch picker)
- Address & amenities — full address, reception phone, check-in/out times, multi-select amenities, interactive Leaflet map for the host to click-drop and drag a pin (writes the property's
latitude/longitude) - Rooms — inline add/edit/delete with name + type + view + beds + size + price + mood + special amenities (comma-separated chips); hero image upload to Supabase Storage. Publish button is disabled until at least one room exists.
Each form is validated by a Zod schema (lib/schemas.js) and submitted with react-hook-form. Failed validations show inline errors on every field, including region/city. The wizard refuses to render at all until the host has filled business_name + phone on their profile (both UI and backend enforce this).
Customers can cancel any pending or confirmed booking from either the profile list or the /reservations/:id screen. Cancellations are soft — the row stays for audit history, just with status = 'cancelled', and shows up under a dedicated Cancelled tab in the profile. The cancelled rows are excluded from the room availability check, so the dates free up immediately.
Nestoria/
├── README.md
├── render.yaml Render Blueprint (web + static + Postgres)
├── docs/
│ ├── ARCHITECTURE.md How the pieces fit together
│ ├── API.md Endpoint reference
│ ├── DATABASE.md Schema, triggers, seed data, migrations catalogue
│ ├── DEPLOYMENT.md End-to-end Render walkthrough
│ ├── SETUP.md Hand-held local install (every prerequisite)
│ ├── logo-light.png Wordmark for GitHub light mode
│ └── logo-dark.png Wordmark for GitHub dark mode
│
├── database/
│ ├── 001_schema.sql Tables, indexes, updated_at trigger
│ ├── 002_triggers.sql Bayesian rating recompute
│ ├── 003_storage.sql Supabase Storage RLS (run on Supabase, not Postgres)
│ ├── 004_seed.sql 8 hotels, 18 rooms, 50 customers, ~100 bookings, ~220 reviews
│ ├── 005_seed_images.sql Auto-generated by scripts/upload-images.js (hero, gallery, room images)
│ ├── 006_saved_hotels.sql `saved_hotels` table for server-side favourites
│ ├── 007_hotel_coords.sql `hotels.latitude` / `longitude` + seed coords
│ └── 008_room_extras.sql `rooms.name` + `rooms.special_amenities`
│
├── scripts/
│ ├── image-manifest.js Auto-generated photo manifest (8 hotels × 5 + 18 rooms)
│ ├── fetch-hotel-photos.js Queries Unsplash Search API → writes image-manifest.js
│ ├── upload-images.js Downloads from Unsplash → uploads to Supabase → writes 005_seed_images.sql
│ ├── print-manifest-urls.js Read-only spot-check helper
│ ├── check-seed-passwords.js bcrypt-verifies every seeded password loads as "password123"
│ └── wipe-seed.js Removes the 8 demo hotels + 50 customers + 3 hosts + matching Supabase files
│
├── backend/
│ ├── server.js Thin wiring layer (~30 LOC)
│ ├── init-db.js Idempotent first-boot schema + seed loader (called by `npm start`)
│ ├── config/ db.js · supabase.js · google.js
│ ├── lib/ http · jwt · userRepo
│ ├── middleware/ auth · error
│ └── modules/ auth/ · hotels/ · rooms/ · bookings/ · reviews/ · profile/ · host/ · upload/
│
└── frontend/
├── index.html
├── vite.config.js
└── src/
├── main.jsx QueryClient + GoogleOAuth + AuthProvider mount
├── App.jsx Router + Header + Footer + TweaksPanel
├── styles/index.css Editorial design system (~1 kLOC, CSS variables)
├── components/ Header · Footer · SearchBar · HotelCard · Icon · Photo · Stepper · TweaksPanel · ThemeToggle · ProtectedRoute · Popover · HotelMap
├── screens/ Home · Hotels · Detail · Booking · Reservation · Login · Profile · Host · AddRooms · About · Journal · JournalPost · Help · Contact · Legal · BecomeHost · NotFound
├── hooks/ useTheme · useTweaks · useSavedHotels
├── lib/ api · queryClient · schemas · content (static editorial copy)
└── context/ AuthContext
For a longer narrative tour with data flows, see docs/ARCHITECTURE.md.
| Location | Size | Notes |
|---|---|---|
| PostgreSQL database | ~5 MB seeded | Schema in database/*.sql |
| Supabase Storage bucket | up to 1 GB on free tier | One public bucket: hotel-images |
The frontend stores three keys in localStorage:
| key | purpose |
|---|---|
nestoria-token |
JWT issued by /api/auth/* endpoints |
nestoria-user |
Cached user object (id, email, full_name, role, onboarded, …) |
nestoria-theme |
light or dark |
nestoria-tweaks |
{ "accent": "terracotta" | "forest" | "ink" | "saffron" | "plum" } |
Favourites used to live in localStorage too — they're now server-side, persisted to the saved_hotels table per (user_id, role). No cookies. No third-party scripts. No localStorage on first visit until you sign in or change a setting.
Nestoria has zero analytics, zero tracking pixels, zero crash-reporting SDKs. Every outbound request the app can make is listed here:
| When | Where | Why | Optional? |
|---|---|---|---|
| Any page load | Your Nestoria backend | API calls (search, detail, etc.) | Required |
| Sign in / sign up | Your Nestoria backend | /api/auth/* |
Required |
| Google sign-in | accounts.google.com |
Google Identity Services popup | Only if you click the Google button |
| Image upload | Your Supabase project | Storage write via service-role | Only if a host uploads an image |
| Image display | <project>.supabase.co |
CDN reads | Yes; falls back to abstract striped placeholders |
| Theme assets | fonts.googleapis.com + fonts.gstatic.com |
Instrument Serif, Geist, JetBrains Mono | Yes; can be self-hosted by replacing the <link> in index.html |
The backend itself only talks to your Postgres, your Supabase project (if configured), and Google's JWKS endpoint (only when verifying an ID token).
If you self-host with no Supabase + no Google sign-in, the only network calls leaving the user's browser are to your own backend.
| script | what |
|---|---|
npm start |
production: node server.js |
npm run dev |
development: nodemon server.js |
| script | what |
|---|---|
npm run dev |
Vite dev server with HMR at http://localhost:5173 |
npm run build |
Production build to ./build |
npm run preview |
Preview the production build locally |
Run from the repo root with NODE_PATH=backend/node_modules so they can resolve dotenv, pg, @supabase/supabase-js, etc. from the backend's modules.
| script | what |
|---|---|
node scripts/fetch-hotel-photos.js |
Hits the Unsplash Search API (needs UNSPLASH_ACCESS_KEY in backend/.env), filters results by hotel/room/interior keywords, rewrites scripts/image-manifest.js with the alt description + photographer annotated in every entry |
node scripts/upload-images.js |
Downloads each URL in the manifest, uploads to the Supabase hotel-images bucket (upsert: true), writes database/005_seed_images.sql |
node scripts/print-manifest-urls.js |
Prints every URL in the manifest so you can spot-check in a browser. Read-only. |
node scripts/check-seed-passwords.js |
Runs bcrypt.compareSync('password123', hash) against every seeded host + customer and prints PASS / FAIL / SKIP (Google-only). Exits non-zero on any failure. |
node scripts/wipe-seed.js --dry-run / --yes |
Removes the 8 seed hotels, 50 demo customers, 3 demo hosts, their 80 bookings, and the matching Supabase Storage objects (hotels/<slug>/, rooms/<slug>/). Run before going live to clear the public password123 accounts. See docs/DEPLOYMENT.md → 5a. |
| Symptom | Fix |
|---|---|
psql: command not found |
Postgres.app and Homebrew install psql outside the default PATH. Add the right directory to your shell — see docs/SETUP.md → Step 3. On macOS with pgAdmin only: export PATH="/Applications/pgAdmin 4.app/Contents/SharedSupport:$PATH" in ~/.zshrc. |
| I have pgAdmin but no Postgres server | pgAdmin is a client only. Install an actual server via Postgres.app, Homebrew, Docker, or use a cloud DB — see docs/SETUP.md → Step 2. |
| Backend boots but every API call returns 500 | Database not initialised. Run psql -f database/001_schema.sql and 002_triggers.sql, or load them through pgAdmin's Query Tool. |
ECONNREFUSED on backend boot |
Postgres isn't running, or DB_HOST/DB_PORT are wrong. For Render, ensure DB_SSL=true. |
CORS error: blocked by Access-Control-Allow-Origin |
CORS_ORIGIN on the backend doesn't match the frontend URL exactly. Trailing slashes matter. |
POST /api/auth/login returns 401 "Invalid credentials" |
The most common cause is the role toggle on the sign-in screen — customer emails need "I'm travelling", host emails need "I'm hosting". The login form now appends a hint reminding you. Run node scripts/check-seed-passwords.js to confirm the hashes themselves are good. |
POST /api/auth/login returns "This account uses Google sign-in" |
The account was created via Google, so it has no password set. Use the Continue with Google button. |
| Host signed in but the "Add property" page just shows "Finish your host profile" | The host must fill business_name + phone on /host/profile before listing. Both the wizard and the backend POST /api/hotels enforce this. |
| Google sign-in popup closes immediately with "origin not allowed" | Add your frontend URL to the OAuth Client's Authorized JavaScript origins in Google Cloud Console. |
/api/upload/* returns 503 "Image upload is not configured" |
Set SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, and SUPABASE_BUCKET_NAME in backend/.env. |
| Booking POST returns 409 "Room is already booked for those dates" | Another booking overlaps. Pick different dates, or cancel the conflicting one as that customer. |
| Reviews POST returns 400 "allowed on completed stays only" | The booking's status isn't completed. The seed file pre-marks some bookings as completed so reviews can land on them; for new bookings you'd need to update the status manually (a job/scheduler is on the roadmap). |
| Frontend page is blank | Open DevTools console. Usually a missing env var (VITE_API_URL). |
vite: command not found |
Run npm install inside frontend/ — the Vite binary is a dev dependency. |
| Theme flashes light → dark on load | The anti-FOUC script in index.html was edited. The block at lines 17–24 must run before the React mount. |
| OKLCH colours look wrong in Firefox | You're on Firefox <113. Update — color-mix() and oklch() need a recent build. |
Things that aren't built but would be welcome contributions:
- Real payments — Razorpay or Stripe in place of the simulated payment terminal in the booking wizard
- Email notifications — booking confirmations, host new-booking alerts (Resend or SES)
- Host inbox — message threads between guests and hosts
- Calendar view for the host's bookings tab
- Internationalisation — currently INR-only and English-only
- Booking lifecycle scheduler — auto-transition
confirmed→completedonce the checkout date passes - Mobile app — the current responsive web UI works on phones, but a wrapped PWA or native app would be nicer
Pull requests welcome. The repo follows a few conventions:
- Backend: feature folders under
backend/modules/. New endpoints live in their own module rather than in a giant controller. - Frontend: one file per screen, shared widgets in
components/. No CSS-in-JS — everything goes insrc/styles/index.cssusing existing utility classes (.btn,.card-flat,.hcard, …) and CSS variables (var(--ink),var(--accent)). - Validation: every form-derived API call should have a matching Zod schema in
frontend/src/lib/schemas.js. - Database: never edit
001_schema.sqlin-place once data is live. Add a new numbered file (005_<name>.sql) instead.
Run both packages locally before opening a PR:
cd backend && npm install && npm run dev
cd frontend && npm install && npm run devThere's no test suite yet (it's on the roadmap). For now, manually verify the golden path: register → search → detail → book → review.
- Built on top of React, Vite, Express, and PostgreSQL — the open-source giants that make small full-stack projects possible.
- Typefaces by Instrument (Instrument Serif), Vercel (Geist), and JetBrains (JetBrains Mono).
- Hosting on Render and Supabase — both have generous free tiers that make self-hosted SaaS realistic.
- Hotel names and editorial descriptions in the seed file are fictional; any resemblance to real properties is a coincidence.
Created and maintained by Avaneesh.
MIT — see LICENSE.txt.