Bali Villas: a free, open-source villa rental booking template

Stanislav TyshchenkoEngineeringAnnouncement12 min readOct 18, 2026
The Lumbung Sidemen villa storefront on a laptop and a phone, on a teak table in a thatched bale above the Sidemen rice terraces with Mount Agung behind, with the title 'A free villa rental booking template'

Most villa owners I talk to have a beautiful website and nowhere to book on it. The photos are great, the pool is great, and then the button says Enquire on WhatsApp or links out to the Airbnb listing. So the guest who found you on Instagram, or who stayed last year and wants to come back, ends up paying through a platform that takes its cut on a booking you already earned.

Bali Villas is a full villa resort website where that button is a real booking. The guest picks a house, picks check-in and check-out on a calendar that knows which nights are taken, adds a sunrise trek or an airport transfer, and pays. It's MIT licensed, it's on GitHub, and the availability, checkout and guest accounts behind it run on Opencals, so you aren't writing a night-counting engine or handling card data yourself.

The Lumbung Sidemen homepage: a drone shot over the Sidemen rice terraces with Mount Agung behind, price pins on four villas from $165 to $420 a night, and a check-in, check-out and guests search bar

The demo brand is Lumbung Sidemen, a made-up family resort of ten houses on the rice terraces of the Sidemen valley in East Bali, priced in US dollars on Bali time. It runs from a $140 rice-barn studio for two to a $520 four-bedroom estate for eight. Every name, price and photo in it is data you replace.

Bali Villas is a Next.js 15 frontend you own (MIT) on a booking backend you don't have to run (the Opencals Storefront API). Each villa is booked by the night with a fixed 15:00 check-in and 11:00 check-out, checked against live availability, with stay extras, guest details and a passwordless "My trips" account. Live demo at template-bali-villas.vercel.app.

A night is 20 hours, and you still charge for a whole one

Hotel software has a habit of hiding the one thing that makes stays awkward to model: a night isn't a day. A guest who arrives on Friday at 15:00 and leaves on Monday at 11:00 has stayed three nights, but only 68 hours. If your booking engine counts in 24-hour blocks, it either charges for two and a bit, or it blocks Monday afternoon when the next guest wants to arrive.

So the template leans on two things the Opencals API already has. Each villa is a product with a base duration of one day (86400 seconds) and allowCustomDuration: true, which makes the night the pricing unit. And each villa has fixed times: fixedTimes: true, with fixedStartTime at 15:00 and fixedEndTime at 11:00. A stay of N nights runs from 15:00 on the check-in day to 11:00 on the check-out day, the backend rounds the booked time up to whole units, and three nights cost exactly three times the nightly rate.

The fixed times are enforced on the server, not just in the calendar. A customer booking or date change at any other time gets a 400, measured in the schedule's timezone. Bookings you make yourself in the dashboard are exempt, which is how an early check-in still gets arranged when a guest lands at 9:00 and asks nicely.

The timezone part is where I'd expect anyone building this from scratch to lose a day. Bali is UTC+8 and the API stores everything in UTC, so the README's example, a stay from 14 to 17 November, goes to the API as:

ts
{ fromDate: '2026-11-14', fromTime: '07:00:00', toDate: '2026-11-17', toTime: '03:00:00' }

That conversion is toStaySlot in lib/stay.ts, and it only ever runs on the server, inside /api/reserve. The browser sends local dates and never builds a slot. lib/stay.test.ts covers the night maths with vitest, which I'd keep running if you change the check-in time.

Check-out day is someone else's check-in day

Because a booked night only blocks 15:00 to 11:00, the morning a guest leaves is free for the next arrival that afternoon. That four-hour gap is your turnover window, and nothing in the calendar has to know about it.

To draw the calendar, the villa page asks for the villa's merged availability ranges (/api/villas/[slug]/ranges) rather than a list of slots. Four small functions then do the work in the browser. isNightAvailable decides which nights get struck through. fitsStay checks that a whole stay sits inside one free range. latestCheckout stops the guest picking a check-out that runs into the next booking. firstFreeStay powers the "next free stay" line on the villa list when your dates are taken. The server checks again when the booking is created, so a stale calendar can't double-book a villa.

The illustrated site map: ten houses numbered on one hillside of terraces, with the list of villas and nightly prices from $140 for Lumbung Padi to $520 for Puri Sidemen

Each villa is its own product in its own product pool, with maxAttendees: 1. It's tempting to model "Rice barns" as one product with variants, but in Opencals variants don't block each other, so two guests could both book the same barn for the same week. Collections (Rice barns, Pool villas, Family houses, Signature) do the grouping instead.

The guests themselves aren't part of the slot either. Adults, children, infants, arrival time, flight number and the occasion travel as appointment customAttributes, with the keys listed in siteConfig.customAttributeKeys. They show up on the booking in the Opencals dashboard and on the guest's trip page. A villa is booked once whether two people sleep in it or eight.

What the guest sees before they pay

The homepage opens with a candi bentar split gate over a drone flyover of the valley. Each villa carries a price pin that follows it through the video, tracked separately for the desktop and mobile clips. Below that come an arched villa carousel, the illustrated site map above, an experiences section, "A day at Lumbung" from morning to night, testimonials and the FAQ. With reduced motion turned on, the video freezes on a still with static pins.

The villa list filters by guests, bedrooms, pool, price and collection, with a map view. Set dates and every card checks its own availability and shows either the total for your nights or the next free stay of that length.

The Joglo Agung villa page: an arched hero photo of a teak joglo and pool facing Mount Agung with a Day and Dusk toggle, next to a gallery of the pool, dining pavilion and bedrooms

The villa page is the one I'd show a client first. A Day and Dusk toggle on the hero, a gallery grouped by room, a room-by-room tour, an SVG floor plan, sleeping arrangements, amenities, house rules, a cancellation timeline and reviews. A sticky reservation card carries a two-month calendar, the villa's minimum stay, a guest picker capped at the villa's maximum and a nightly × N breakdown. On a phone it becomes a bottom bar that opens a sheet.

Checkout is one page at /reserve/[slug], in four steps: the stay (dates and guests, editable inline), extras as photo cards, the lead guest with arrival details and your checkout questions, then payment through Stripe Elements with a promo code and cash or no-payment fallbacks. The demo extras are an airport transfer ($45), a floating breakfast ($28), a sunrise trek up Mount Agung ($55), a cooking class ($35), a massage for two ($60), a romance setup ($40), and early check-in and late check-out requests ($35 each). Extras set up with durationMultiplied are charged once per night, the rest per stay or per item.

The reserve page for Joglo Agung: a two-month stay calendar with 2 to 5 November selected and booked nights struck through, and a green summary panel with check-in Mon 2 Nov from 15:00, check-out Thu 5 Nov by 11:00, $420 a night and free cancellation until Sun 18 Oct

The summary rail shows the itemised total with taxes exactly as the store charges them, the last day of free cancellation and a hold timer while the villa sits in the guest's cart. The confirmation page has the itinerary, an .ics download, directions and a pre-arrival checklist.

Guests get trips, not order numbers

Sign-in is passwordless: a 6-digit code by email, or a one-time link. The account is built around the next stay, with a countdown, the arrival details and quick actions, then upcoming and past trips.

A trip page for a three-night stay at Pondok Bambu: Change dates and Cancel stay buttons, the stay details with arrival time and extras, an airport transfer card and the payment summary of $821

A trip page holds the dates, guests, extras and payments, plus the villa's house manual (wifi, quirks of the house, "bring a torch for the stairs at night") and directions. The guest can change dates or cancel inside your policy window, which comes from the product's cancelGap, 14 days in the demo. After the stay they can leave a review. Every link in Opencals emails (view, change, cancel, feedback) resolves through /link/[token], which signs the guest in and drops them on the right page.

What it leaves on your desk

These are the gaps I'd want to know about before putting this in front of real guests.

It doesn't sync with Airbnb or Booking.com. There's no iCal feed and no channel manager connection in the template. If the same villa is also listed on a platform, nothing here keeps the two calendars in step, and you'll have to block dates by hand or risk selling the same night twice. For a property that lives on OTAs, that's the first thing to solve.

Changing dates keeps the length. A guest can move a three-night stay to any other free three nights, but shortening or extending is a message to you. The template refuses it on purpose: the backend doesn't reprice an order on reschedule, so a longer stay at the old price is the bug you'd otherwise ship.

Extras can't be added after booking. Once the booking is made, the trip page says to message you for the airport transfer, and you add it yourself.

One rate per villa. The price is nights × the nightly rate + extras. There's no high-season rate, no weekend rate and no weekly discount in the template. For Bali, where August and Christmas are a different market from February, that matters.

The minimum stay is a template rule. Two or three nights, set per villa in lib/villa-content.ts, along with the guest limit. The site enforces it, but it lives in the template rather than the store, so a booking made outside the site isn't held to it.

No damage deposit. Nothing is pre-authorised at checkout. If you hold a deposit, it's done at the door.

And the two that apply to every Opencals template: Opencals itself is not open source, only the templates and the SDK are, and the free Dev Store runs in test mode, so it can't take real payments until you move to a live store.

Airbnb's 15.5% against a dollar a booking

Airbnb's own resource centre puts its host fee at a single 15.5% in 2026, taken from the payout and applied to the nightly price and fees like cleaning (checked 18 October 2026; listings in Brazil and Mexico pay 16%). On three nights at Joglo Agung, $1,260, that's $195.30.

Opencals pricing, from opencals.com/pricing, checked the same day:

Elastic is $0.99 per booking with no monthly fee and every feature included. Three nights at Joglo Agung cost you 99 cents plus your card processor's fee. For one to three villas, this is the plan.

Build your plan is $14.98 a month including 300 bookings, then $0.20 each, with features as add-ons. A villa site needs Flexible duration ($9.98/month) for multi-night stays and Customer self-service ($14.99/month) for guests changing dates and cancelling. That's $39.95 a month, which beats Elastic once you pass about 40 bookings a month. Ten villas averaging a booking a week each gets you there.

The comparison isn't fair to Airbnb, and I'd rather say so. Airbnb finds you guests; a website on your own domain finds you nobody. What the template does is stop you paying the platform's rate on the guests who were already yours: the repeat visitors, the ones from Instagram, the friend of last year's honeymooners. Keep the listings for discovery and send everyone who already knows your name to your own site.

Two files, and the photos live in your store

Two files carry nearly all the business-specific work. lib/site-config.ts holds the name, currency and timezone, check-in and check-out times, the longest stay, arrival time windows, occasions, contact details, collections and the hero captions. lib/villa-content.ts holds what the API doesn't model, per villa: the story, size, beds and baths, minimum nights, the rooms with their photos, amenities, house rules, the floor plan, its spot on the site map and the house manual.

Villa photos come from your Opencals store, not the repo, named by room (joglo-agung-pool, for example) so the gallery can group them. Changing a photo is a dashboard job. The editorial art and the hero film ship in public/. If you replace the hero video, the price pins need re-tracking, and there's a script for that in the Opencals repo, or you swap in static pins.

app/globals.css holds the design tokens as Tailwind v4 @theme properties, with the full design system written up in DESIGN.md:

css
@theme { --color-paper: #f4eee4; /* limestone page */ --color-jungle: #1f3a2e; /* dark sections, footer, checkout summary */ --color-terracotta: #b4502e; /* the only accent */ }

The stack under it is the same as the other templates:

LayerWhat it uses
FrameworkNext.js 15 (App Router), React Server Components
StylingTailwind CSS v4: 24px cards, 20px images, pill buttons
TypeFraunces (opsz, SOFT, WONK axes) and Figtree
MotionFramer Motion + Lenis, with reduced-motion fallbacks
Datesmoment-timezone
Formsreact-hook-form + Zod
PaymentsStripe Elements, loaded on demand
AuthNextAuth.js v5, passwordless by default
Booking dataOpencals Storefront SDK v0.3.17

LodgingBusiness, VacationRental and Accommodation structured data, Open Graph cards, robots.txt and sitemap.xml ship configured.

The preset seeds all ten houses

1

Create a Dev Store with the Bali Villa Rentals preset

Sign up at app.opencals.com, create a Dev Store and pick the Bali Villa Rentals preset (category Villa resort) when it asks for a dataset. It seeds the ten villas on a 24/7 schedule with fixed check-in and check-out, the four collections, the stay extras, the checkout questions and sample bookings, so the site renders like the demo.

2

Generate a Storefront API key

In your Opencals account settings, generate a Storefront API key. It starts with sfk_ and is scoped to that store.

3

Deploy to Vercel

Use the button below. Vercel asks for OPENCALS_API_KEY and AUTH_SECRET: paste the sfk_ key and any random string. Add a Stripe publishable key when you want card payments, then set url in lib/site-config.ts to your domain.

Deploy with Vercel

Set the Storefront Base URL before the first real guest

In your Opencals dashboard (Settings → API), set Storefront Base URL to your deployed domain. Every emailed link (view the stay, change dates, cancel, leave a review) is built as {storefrontBaseUrl}/link/{token}. Left on localhost, they go nowhere.

The setup docs cover running it locally. If you're building with an AI agent, the rentals-multi-day reference in the Opencals agent skills describes the same nightly-stay pattern.

Bali Villas is the seventh template in the series. NOIR books cars by the day across a date range, and it's the closest relative: same range-based availability, but midnight to midnight with delivery windows instead of fixed check-in. VOLT books padel courts by duration on a grid. Haar, Frisor and Clarity are appointment templates built around staff, and Fluently adds group classes with live seats.

If you run villas, the question this template puts to you is how many of last year's guests would have booked direct if your own site had let them. If you build sites for villa owners, it's a working reference for nightly stays on a headless booking backend, and lib/stay.ts is about 200 lines you can lift into your own design.

Frequently Asked Questions

Get started

Ready to transform your service business?

Join 150+ businesses already using Opencals. Start on a free development store with every feature unlocked, and only pay once you go live.

No credit card required
Setup in 10 minutes
Cancel anytime