salon

Salon booking app with specialists, schedules, Stripe payments and English and French localization

salon payloadreserve demo
1 / 1
salon payloadreserve demo

Overview

Summary generated from the README

A booking site for a salon built with Payload 3, Next.js 15 and MongoDB, using the payload-reserve plugin for services, specialists, schedules and reservations. Customers choose a service, specialist and time slot, then pay through Stripe Checkout. It needs Node 20.9 or newer, pnpm, a local MongoDB and the Stripe CLI for webhook testing. Email goes through Resend and the frontend is localized with next-intl.

Features

  • Services, specialists, per-specialist weekly schedules and reservations through the payload-reserve plugin
  • Booking flow with Stripe Checkout and a webhook that confirms paid reservations
  • Scheduled Payload jobs that remind customers about abandoned payments and cancel stale pending bookings
  • Confirmation and cancellation emails sent through Resend
  • English and French frontend routes using next-intl
  • Seed script, Vitest integration tests and Playwright end-to-end tests

Good for

  • Building appointment booking sites for salons, spas or clinics with staff-specific availability
  • Learning how to integrate the payload-reserve plugin with Stripe payments and background jobs

From the author

Lumière Salon — payload-reserve demo: specialists, per-specialist schedules, buffer time, i18n

Get started

npx giget@latest gh:elghaied/payloadreserve-demos/apps/salon my-app

README

Lumiere Salon — payload-reserve Demo

Live: salon.payloadreserve.com

A full-stack booking app built with Payload CMS 3.x + Next.js 15 (App Router). It is the reference implementation for the payload-reserve plugin and lives inside the payload-reserve-demos monorepo.


Tech Stack

LayerTechnology
FrameworkNext.js 15 (App Router, React 19)
CMS / APIPayload CMS 3.x
DatabaseMongoDB (via @payloadcms/db-mongodb)
Pluginpayload-reserve v1.1.0
PaymentsStripe Checkout
EmailResend
i18nnext-intl (English + French)
StylingTailwind CSS v4
TestingVitest (integration) + Playwright (e2e)

Prerequisites


First-Time Setup

# 1. Install dependencies (run from monorepo root)
pnpm install

# 2. Copy the env file and fill in your values
cp apps/salon/.env.example apps/salon/.env

# 3. Seed the database (services, specialists, schedules, sample data)
cd apps/salon && pnpm seed

Required environment variables (apps/salon/.env)

# MongoDB
DATABASE_URL=mongodb://127.0.0.1/payloadreserve-salon-demo

# Payload
PAYLOAD_SECRET=any-random-secret-string

# Stripe (test keys from dashboard.stripe.com)
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...   # ← filled automatically by stripe-listen.sh (see below)

# Email (optional — leave blank to skip sending emails)
RESEND_API_KEY=
[email protected]
RESEND_FROM_NAME=Lumière Salon

# App URL
NEXT_PUBLIC_SITE_URL=http://localhost:3000

Starting the Dev Environment

Press Ctrl+Shift+B (or Ctrl+Shift+P → "Tasks: Run Task" → dev:salon).

This opens two terminal panels in parallel:

TerminalCommandWhat it does
salon: next devpnpm dev:salonStarts the Next.js + Payload dev server on port 3000
salon: stripe listenbash scripts/stripe-listen.sh salon 3000Forwards Stripe webhooks to localhost and auto-updates .env

Manual (two separate terminals)

# Terminal 1 — from monorepo root
pnpm dev:salon

# Terminal 2 — from monorepo root
bash scripts/stripe-listen.sh salon 3000

Important: both processes must be running at the same time when testing payments. Stopping stripe listen means Stripe events never reach the app and reservations stay pending.


How scripts/stripe-listen.sh Works

The Stripe CLI generates a new webhook signing secret every time stripe listen starts. Without the correct secret, the webhook handler rejects all incoming events (signature mismatch → 400).

The script solves this automatically:

  1. Runs stripe listen --forward-to localhost:3000/api/stripe-webhook
  2. Parses the whsec_xxx secret the CLI prints on its first line of output
  3. Writes STRIPE_WEBHOOK_SECRET=whsec_xxx into apps/salon/.env
  4. Next.js dev server detects the .env change and restarts within seconds

After that the full payment flow works end-to-end:

User pays → Stripe → CLI tunnel → POST /api/stripe-webhook → status: confirmed

All Commands

From monorepo root

pnpm dev:salon           # Start salon dev server (turbo)
pnpm build:salon         # Production build
pnpm build               # Build all apps
pnpm lint                # Lint all apps
pnpm generate:types      # Regenerate payload-types.ts (run after schema changes)
pnpm generate:importmap  # Regenerate admin import map (run after adding admin components)

From apps/salon/

pnpm dev          # Start dev server
pnpm devsafe      # Wipe .next cache then start dev (use when hot-reload breaks)
pnpm seed         # Seed database with services, specialists, schedules
pnpm test         # Run all tests (integration + e2e)
pnpm test:int     # Vitest integration tests only
pnpm test:e2e     # Playwright e2e tests only
npx tsc --noEmit  # Type-check without building

Project Structure

apps/salon/src/
├── app/
│   ├── (frontend)/[locale]/     # Public-facing pages (book, account, cancel, success)
│   │   └── book/
│   │       ├── actions.ts       # Server actions: create reservation, Stripe checkout, slots
│   │       ├── cancel/          # Cancel page (pending reservations only)
│   │       └── success/         # Post-payment confirmation page
│   ├── (payload)/               # Payload admin panel + REST/GraphQL API routes
│   └── api/stripe-webhook/      # POST handler for Stripe checkout.session.completed
├── collections/                 # Payload collection configs (Users, Media, Gallery, …)
├── email/templates/             # HTML email templates (confirmation, cancellation, abandonedPayment)
├── globals/                     # Payload globals (Homepage, SiteSettings)
├── hooks/
│   └── reservationNotifications.ts  # Plugin callbacks → send emails on confirm/cancel
├── i18n/                        # next-intl routing + request config
├── seed/                        # Seed scripts and static data
├── tasks/
│   ├── cancelStaleReservations.ts   # Payload job: cancels unpaid pending reservations after 30 min
│   └── notifyAbandonedPayments.ts   # Payload job: emails customers who haven't completed payment
├── payload.config.ts            # Main Payload config — plugin, collections, email, jobs
└── payload-types.ts             # Auto-generated — DO NOT edit manually

Path aliases (in tsconfig.json)

AliasResolves to
@/*./src/*
@payload-config./src/payload.config.ts

How the Booking Flow Works

1. User selects service + specialist + date/time
2. createReservation() creates a Reservation with status: "pending"
3. Stripe Checkout session is created (metadata: { reservationId })
4. User pays with test card 4242 4242 4242 4242
5. Stripe fires checkout.session.completed webhook
6. /api/stripe-webhook updates Reservation to status: "confirmed"
7. Success page shows confirmation

payload-reserve plugin collections (auto-created)

SlugDescription
servicesTreatments/services with price, duration, category
specialistsStaff members linked to services they offer
schedulesWeekly availability per specialist
reservationsBooking records with status, startTime, customer, service, resource
customersCustomers (separate from admin users)

Background Jobs

Two Payload jobs run on a schedule to handle abandoned checkouts:

JobScheduleWhat it does
notifyAbandonedPaymentsEvery 5 minEmails customers whose reservation has been pending for 5+ minutes without payment
cancelStaleReservationsEvery 15 minCancels reservations still pending after 30 minutes

The abandoned payment reminder uses a paymentReminderSent flag on the reservation (added via extraReservationFields) to guarantee exactly one email per reservation, regardless of server restarts or cron timing.

Defined in: src/tasks/


Email Notifications

Emails are sent via Nodemailer. Hook callbacks in src/hooks/reservationNotifications.ts fire on:

EventEmail sent
Booking confirmed (paid)"Booking confirmed"
Booking cancelled"Cancellation"

The abandoned payment reminder is handled separately by the notifyAbandonedPayments job (src/tasks/notifyAbandonedPayments.ts).

Leave RESEND_API_KEY blank in development to skip email sending silently.


After Schema Changes

Always run these two commands after modifying any collection/global config:

# From monorepo root:
pnpm generate:types       # Updates src/payload-types.ts
pnpm generate:importmap   # Updates admin import map (only needed for admin UI components)

URLs

URLDescription
http://localhost:3000Frontend (defaults to /en)
http://localhost:3000/frFrench locale
http://localhost:3000/adminPayload admin panel
http://localhost:3000/apiPayload REST API

More Business & agency templates

events screenshot

events

Event ticketing and venue booking app built on Payload, Next.js and the payload-reserve plugin

0
v3
Next.js
Updated 2 months ago
hotel screenshot

hotel

Hotel booking site with room pools, multi-night date-range reservations and Stripe checkout

0
v3
Next.js
Updated 2 months ago
restaurant screenshot

restaurant

Restaurant reservation site with party-size bookings, dining experiences, and Stripe checkout

0
v3
Next.js
Updated 2 months ago