A small, password-protected site for gifting or sharing a vacation week: a gallery, amenities list, a calendar of available weeks, and a booking form that emails the request to you and confirms it to the requester. Built with Next.js, TypeScript, and Resend — no database required.
Originally built to gift a timeshare week as a wedding present; generalized here so anyone can fork it for their own property, cabin, or vacation week.
pnpm install
cp .env.example .env.local # fill in the values, see below
pnpm devOpen http://localhost:3000. Enter the password you
set in .env.local to see the site.
Everything you'd want to change for a basic reskin lives in one file:
src/content/site.ts — the hero copy, property
description, amenities, gallery captions, and email text. Edit that file,
save, and the site reflects it.
The available date ranges live in src/lib/dates.ts
instead, since the server also validates bookings against that exact list —
see the comment at the top of the file for why it's kept separate.
Gallery and hero images under public/images/ are free
stock photos from Unsplash (no attribution required under the Unsplash
License, credited here anyway): the hero is by
Yifan Ma, and the gallery photos are by Alan
Zhang, Christine von
Raesfeld, Sterling Lanier, Colton
Jones, and Tran
Nguyen. They're generic coastal shots,
not the real property — drop your own JPGs/PNGs in there with the same
filenames (or update the paths in content/site.ts) to replace them. If
you'd rather host images externally, add the image host to
images.remotePatterns in next.config.js and use next/image or an
<img> tag pointing at the remote URL.
Copy .env.example to .env.local and fill in:
| Variable | What it's for |
|---|---|
SITE_PASSWORD |
The password gating the whole site. Also used to sign session cookies — changing it invalidates existing sessions. |
RESEND_API_KEY |
API key from resend.com (free tier available) for sending booking emails. |
OWNER_EMAIL |
Where booking requests get sent. |
FROM_EMAIL |
The "from" address on outgoing emails — must be a verified sender/domain in Resend. |
In production (Vercel or otherwise), set these in your host's environment
variable settings rather than committing .env.local.
This isn't a toy password gate — a few things are handled deliberately:
- Real sessions, not client-side state.
/api/authsets an httpOnly, signed session cookie on correct password. The booking API checks that cookie server-side rather than trusting anything the client claims. - Rate limiting on both the login and booking endpoints (in-memory,
per-IP — see the note in
src/lib/rate-limit.tsabout its limits on serverless deployments). - Constant-time password comparison to avoid leaking password length or content via response timing.
- Input sanitization: all user-submitted text is escaped before being interpolated into the HTML emails, and the submitted booking date is looked up against the server's own canonical list rather than trusted as-is from the client — closing what would otherwise be an open endpoint that could send arbitrary emails to arbitrary addresses.
- Security headers (
X-Frame-Options,X-Content-Type-Options,Referrer-Policy,Permissions-Policy, HSTS) set innext.config.js.
If you fork this for something with actually sensitive data behind it, treat the password gate as a "keep casual visitors out" mechanism, not enterprise-grade access control — there's no user accounts, no audit log, and one shared password.
Works on Vercel with zero config beyond the environment variables above:
- Next.js (App Router) + React + TypeScript
- Resend for transactional email
- No database — available dates are a static list, bookings are emailed, not stored
MIT — see LICENSE. Fork it, use it for your own property, change whatever you want.