Open-source booking and property-management platform for small hospitality businesses — guesthouses, B&Bs, small hotels. Self-hostable, MIT-licensed, built on Next.js and Supabase.
openstay was extracted from a real, in-production booking system for a small Business. It runs a real occupancy calendar, real bookings, real guest check-ins, and a real channel sync with Airbnb every day. It is deliberately small in scope — no giant SaaS back office, no per-seat pricing, no vendor lock-in. Clone it, connect your own Supabase project, deploy to Vercel, and you own the whole stack.
- You own your data. Everything lives in your own Supabase project (Postgres, under your control) instead of a third-party SaaS.
- No paywalled features. Payments, e-mail notifications, and channel sync are all included — each is simply optional and env-gated, so an unconfigured instance still works fully for on-site/manual bookings.
- Built for one property, not a marketplace. The whole UI is scoped to a single business with an owner/staff team, not a multi-tenant platform.
- Occupancy calendar — scrollable, multi-week view with a fixed room column, color-coded bookings, and inline Airbnb block details.
- Booking management — create, edit and cancel bookings with automatic
room assignment and double-booking prevention enforced at the database
level (a Postgres
EXCLUDE/ GiST constraint, not just app-level checks). - Public booking site — guests search availability and book directly; optional Stripe checkout, or pay on-site.
- Guest self-check-in — a shareable QR/token link
(
/checkin/[token]) lets guests fill in their digital registration form (Meldeschein-style) before arrival. - Channel sync (iCal, 2-way) — export a per-room iCal feed to Airbnb,
Booking.com or VRBO, and import their calendars back to block out rooms
automatically. See
docs/channel-sync.md. - E-mail notifications — guest booking confirmations and owner alerts via Resend (optional).
- Meal plans — settings-driven per-person/night pricing (none / half board / full board), editable by the owner.
- Tourist tax — optional per-person/night flag and pricing, configurable in settings.
- Roles —
ownerandstaffroles via Supabase Auth, enforced with Postgres row-level security (RLS), not just UI checks. - "Today" view — arrivals, departures and in-house guests at a glance.
- PWA / offline shell — installable on staff phones/tablets, with an offline app shell.
- Next.js 16 (App Router). Note: this Next.js
version has breaking changes vs. older releases —
src/proxy.tsreplacesmiddleware.ts. SeeAGENTS.mdbefore touching routing code. - React 19
- Supabase — Postgres, Auth, Row Level Security;
schema managed as SQL migrations in
supabase/migrations/ - Tailwind CSS v4
- Stripe — optional, env-gated online payments
- Resend — optional, env-gated transactional e-mail
- Vitest for unit tests
git clone https://github.com/<your-org>/openstay.git
cd openstay
npm installCreate a new project at supabase.com, then link your local checkout to it and push the schema:
npx supabase login
npx supabase link --project-ref <your-project-ref>
npx supabase db pushThis applies every migration in supabase/migrations/ (roles, rooms,
bookings, room blocks, guests, check-in forms, iCal sync, settings, ...) and
enables the required Postgres extensions (pgcrypto, btree_gist).
Optionally seed demo data (three sample rooms and baseline settings) by
pasting the contents of supabase/seed.sql into the Supabase SQL editor, or:
psql "<your-connection-string>" -f supabase/seed.sqlcp .env.example .env.localAt minimum, set NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY
and SUPABASE_SERVICE_ROLE_KEY from your Supabase project's Settings →
API page. Everything else (branding, Stripe, Resend, iCal, cron) is
optional — see docs/self-hosting.md for the full
reference.
npm run devOpen http://localhost:3000.
Sign up through the app's login page (Supabase Auth). A database trigger
(handle_new_user, defined in
supabase/migrations/20260707151549_init_schema.sql) automatically creates a
matching row in public.profiles with the default role staff. Promote that
first account to owner once, via the Supabase SQL editor:
update public.profiles
set role = 'owner'
where id = (select id from auth.users where email = 'you@example.com');Every subsequent signup starts as staff; only an existing owner can
promote others (do it the same way, or build an admin UI for it).
openstay deploys cleanly to Vercel:
-
Import the repository into Vercel.
-
Add the environment variables from
.env.local(or a subset — seedocs/self-hosting.md) to the Vercel project. -
vercel.jsonalready defines a daily cron job:{ "crons": [{ "path": "/api/cron/airbnb-sync", "schedule": "0 4 * * *" }] }This calls
/api/cron/airbnb-sync, which pulls each room's configured Airbnb iCal URL and mirrors booked dates intoroom_blocks. SetCRON_SECRETin your Vercel environment variables — Vercel automatically sends it asAuthorization: Bearer <CRON_SECRET>on cron invocations, and the route rejects requests without a matching secret once it is set. -
Deploy.
Any other Node.js host that supports Next.js 16 works too; you'll just need
to trigger /api/cron/airbnb-sync yourself (e.g. via cron-job.org calling
/api/cron/airbnb-sync?secret=<CRON_SECRET>) instead of relying on Vercel
Cron.
All three are fully optional — the app runs without any of them, with the corresponding feature simply disabled.
- Stripe (online payments). Set
STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRETandNEXT_PUBLIC_STRIPE_ENABLED=true. Without a configuredSTRIPE_SECRET_KEY, online payment stays disabled and guests can only pay on-site; the public checkout UI reflects this automatically. Point a Stripe webhook at/api/public/stripe-webhookfor thecheckout.session.completedevent. - Resend (transactional e-mail). Set
RESEND_API_KEYto enable guest booking confirmations and owner notifications. SetOWNER_NOTIFY_EMAILto receive owner alerts (new bookings, payments). Without a verified sending domain, Resend only delivers to the account's own address — setRESEND_FROMonce you've verified a domain. - iCal channel sync (Airbnb / Booking.com / VRBO). Each room gets its own
export feed at
/api/ical/<token>(paste this into the channel's "import calendar" field) and can optionally import the channel's own iCal URL (rooms.airbnb_ical_url) to block dates here. Seedocs/channel-sync.mdfor details.
Full env var reference: docs/self-hosting.md.
See CONTRIBUTING.md.
MIT — see LICENSE.