01
Context
Quillmoor is a fictional marketplace built to show a complete booking system on WordPress. It lists two kinds of place: stays such as cabins, villas and lofts, booked by the night, and places to eat such as restaurants, cafes and wine bars, booked by the table for a party size at a time slot. A guest searches, opens a listing, holds nights or a table for ten minutes and pays a deposit. Hosts confirm requests, check guests in, block dates and refund deposits from their own dashboard.
The demo is public at booking.oldev.site, with a banner on every page and a nightly reset. Nothing on it is real: the brand, places and hosts are fictional, Stripe runs in test mode only, and payment is not set up on the public demo yet.
02
The Challenge
- Two booking models in one engine: nights for stays and seats in a time slot for tables, with one state machine behind both.
- Double bookings had to be impossible, not unlikely: two guests can try to take the same night or the last seats in a slot in the same second.
- Hosts need a real back office, but each host may only see and change their own listings, uploads and bookings.
- What a host previews has to be exactly what a guest pays, across seasonal rates, extras and deposit rules.
- A public demo must never take a real payment by accident.
03
Scope and Approach
Listings and editor
One listings post type with locations, categories and amenities. The listing editor uses Secure Custom Fields groups registered in code (galleries, repeaters, dates and opening hours), so every install has identical fields.
Search and listing pages
Search by where, when and who, a category row, a photo grid and Leaflet maps with OpenStreetMap tiles. With dates set, the grid shows only places that are free. A custom block theme, quillmoor, renders it all on WordPress 7.1 and PHP 8.5.
Booking engine
A custom plugin, quillmoor-booking, moves each booking through a state machine: quote, hold, deposit paid, requested, confirmed, arrived and completed, or declined, cancelled and no-show. Holds last ten minutes. Instant book confirms at once; otherwise the host has 24 hours to answer.
Pricing and deposits
Seasonal pricing and extras are set per listing. Deposits go through WooCommerce 11 and the WooCommerce Stripe Gateway, in test mode only, and hosts refund cancelled or declined deposits through the gateway.
Dashboards and roles
Admin, host and guest dashboards. The host role is limited to its own listings, and a manager role runs locations, categories and amenities without being a WordPress administrator. Guests download PDF quotes and confirmations.
Automation and delivery
Booking events go to n8n through a signed outbox, and a separate Laravel 13 dispatch service answers through signed callbacks. Everything ships as Docker images on a self-hosted Coolify server, with a nightly demo reset.
04
Architecture flow
05
Technical Deep Dive
The database decides who gets a night
A stay claims all its nights in one multi-row insert against a unique key on listing and night. If any night is taken, the whole statement fails, so there is no gap between checking and writing and no half-claimed stay. Host blocks live in the same table, so a blocked night can never be booked.
The database decides who gets a seat
Each time slot is a row with a capacity and a booked count. Seats are taken with a conditional update that only succeeds while the slot has room, so a party is refused the moment the slot would overflow. A claim row is deleted before its seats are returned, so racing expiry sweeps cannot give seats back twice.
Prices are never computed in the browser
The booking card asks the server for every price breakdown, and the host price preview uses the same route, so what a host previews is what a guest pays.
Signed messages that survive JSON parsers
The payload travels as a JSON string inside JSON, so its exact bytes reach the verifier even through an n8n webhook. Every receiver checks the key id, a 300-second timestamp window, the signature in constant time, and then a single-use nonce.
06
Engineering Decisions
WordPress owns the booking state
n8n and the dispatch service can only ask for changes through signed callbacks, and every change passes through one state machine. There is one source of truth for every booking.
Live payments are impossible by construction
Live Stripe keys are refused at boot and on save, and a live key closes every booking route and removes every gateway. On a demo, that is the only safe default.
Field groups are code
The listing fields are registered in code, so they are versioned and identical on every install, and the demo seeder writes the same layout.
The demo says what is not set up
Without Stripe keys, a hold still works and shows real availability, and the payment step says payments are not set up instead of failing. Without bot check keys, guests sign in to book.
07
Results
- 389 WordPress integration tests and 21 Laravel tests passing, with PHPCS on WordPress coding standards and PHPStan at level 6.
- Double-booking protection verified by a parallel concurrency test against the running stack.
- A mutation check breaks each security rule in turn and confirms the tests catch it.
- Playwright browser tests at desktop and phone width.
- Built to extend: new listing types, fields as Secure Custom Fields groups in code, mail and CRM transports behind filters, REST endpoints for other front ends or apps, and n8n hooks for reminders, follow-ups and alerts.
- A public demo with free Unsplash and Pexels photos, each credited on the photo credits page.
08
Planned next
- Stripe test keys on the public demo, so the deposit step runs end to end.
- Bot check keys, so guests can book without signing in.