WP-Appointments
WP-Appointments, is a custom WordPress booking plugin for anyone who takes appointments. It provides a fully self-hosted booking flow that lives directly inside a WordPress Divi site — no external API keys, no monthly subscriptions, no data leaving your own server.
Oh, and the entire thing was vibe-coded. Every single line. I will explain what that means and why you should be both impressed and mildly concerned.
Rationale
A family member needed online bookings for their small practice. They were using a third-party scheduling service and it worked fine, but it felt like a lot of overhead for something that is, at its core, a pretty simple problem: show available time slots, let someone pick one, send a confirmation email. Paying a monthly subscription for that felt unnecessary, especially when they already had a WordPress site with Divi on it.
So I figured I’d just build it. Famous last words, right? :p
What started as “just a booking form” turned into a pretty complete system: a multi-step booking widget, an availability engine, transactional emails, self-service rescheduling via tokens, an admin panel with a full audit log, rate limiting, double-booking protection, and a WP-Cron driven reminder system. No payments — that’s intentionally out of scope. Some things are better left simple.
Like Samwise Gamgee once said: *”I can’t carry the integration for you, but I can carry you.”* That’s basically what this plugin does. It carries the booking flow so the user doesn’t have to.
On Vibe-Coding
Here’s the thing. I once made a vow, a solemn, binding, blood-oath of such terrible weight that even the Dunlendings would have caked their pants and never dared break it — that I would never, ever, write a single line of PHP in my life anymore. Not one. PHP and I were done years ago.
This plugin is however, written entirely in PHP.
Anyway. It was written with Claude Code, Anthropic’s AI coding assistant. Every model, every controller, every test, every migration, every email template — all of it. I described what I wanted, it wrote the code, I read it, told it what was wrong, it fixed it, and we went back and forth like that until we had something I was comfortable with.
Is that cheating? Probably. Do I care? Not really. The code is relatively clean, well-tested, and architecturally acceptable. The AI didn’t just slap something together — it followed proper WordPress patterns, applied constructor injection for testability, implemented constant-time token comparison to prevent timing attacks, and even caught a rate limiter vulnerability.
*”One does not simply vibe-code into Mordor”*, said no one ever, because apparently you absolutely can. You just need to review every pull of the Ring — I mean every line of code — before you commit it.
In all seriousness though: I read large parts of the code it produced. I pushed back on architectural decisions I disagreed with. I asked it to explain things I didn’t understand. I caught bugs. I shaped the design. The AI was the keyboard; I was still the developer. It’s just a very fast, very opinionated keyboard that occasionally wants to add unnecessary abstractions.
Much like Grima Wormtongue, the AI sometimes offers counsel that sounds reasonable until you think about it for a moment. You have to stay alert.
DevLog
The Booking Widget
The frontend is a self-contained IIFE in vanilla JavaScript. No React, no Vue, no build step — just a single script file that gets enqueued by WordPress. I considered reaching for a framework but honestly the state machine for a five-step form isn’t complicated enough to justify it. The steps are: pick a service, pick a date, pick a time slot, fill in your details, confirm. That’s it.
The widget embeds as a Divi module (so it shows up in the Visual Builder) with a shortcode fallback for anyone not on Divi. The CSS uses custom properties for theming so you can restyle the whole thing from a single block of overrides without touching the plugin files.
The AI initially suggested we use React for the widget. I said no. It suggested Vue. I said no. It suggested at least a build pipeline. I said absolutely not, this is a booking form, not the Death Star control panel. We settled on vanilla JS and everyone was happier for it.
Availability Engine
This was the part I found most interesting to think through. The core question is: given a date and a service, which time slots are actually free?
The answer involves three layers. First there’s a weekly availability template — open hours per day, with support for multiple windows (e.g. 09:00–12:00 and 14:00–18:00). Then there are one-off blocked slots for holidays or personal days. Then there are existing bookings. Candidate slots are generated from the template, filtered against blocked slots, and filtered again against existing bookings using a standard overlap check: `start < obstacle.end && end > obstacle.start`. What’s left is what you can actually book.
The same engine powers both the frontend availability lookup and the server-side validation at booking time, so there’s no gap between what the widget shows and what the server accepts. The Force is strong with this one. Or at least, the logic is consistent between layers, which is basically the same thing.
Double-Booking Protection
Once the availability check passed I had a window where two concurrent requests could both see a slot as free and both try to book it. The classic race condition.
The fix is a `SELECT … FOR UPDATE` inside a transaction. Before inserting, the server grabs a lock on any overlapping rows. A second concurrent request for the same slot blocks until the first transaction commits or rolls back. It’s the right tool for the job and MySQL supports it out of the box, so I didn’t need to pull in any extra machinery.
Admittedly, the chance of two people simultaneously trying to book the same slot at a one-person practice is roughly equivalent to the chance of the Flying Spaghetti Monster materialising in your kitchen. But the race condition exists in theory and it costs nothing to close it, so we closed it. May you be touched by His Noodly Appendage in gratitude.
Token-Based Rescheduling
Customers can reschedule their own appointments via a link in their confirmation email. The link contains a random token — 32 bytes from `random_bytes()`, so 256 bits of entropy — but only the SHA-256 hash of that token is stored in the database. The comparison uses `hash_equals()` to prevent timing attacks. Tokens expire after 72 hours, get consumed (rotated) after each successful reschedule, and are nulled out on cancellation so a stale link can’t resurrect a cancelled booking.
It’s probably more token security than a small massage practice strictly needs. We are, after all, protecting appointment times and not the One Ring. But as Gandalf once said: *”A wizard is never late, nor is he early; he arrives precisely when he means to.”* And a properly rotated, hash-verified reschedule token arrives precisely when it means to as well. I’m not entirely sure that analogy holds up but I’m going with it.
Email System
All transactional emails are decoupled from the booking logic via WordPress action hooks. The booking controller fires `wpappt_booking_created` and has no idea who’s listening. The email service hooks in at priority 10 and handles everything from there. This means you could hook in from `functions.php` to push to a CRM or send an SMS without touching the plugin.
There are nine email templates in total covering new bookings, confirmations, cancellations, reschedules, reminders, and admin follow-ups. The sender name is sanitized with `email_header_value()` to prevent header injection. I’ll be honest, that one I had to look up — it’s not something you think about until you think about it. The AI knew about it immediately. Slightly unsettling, but useful.
Security
A few things I added after doing a proper security pass — which, by the way, the AI also helped with. I asked it to research common WordPress vulnerabilities, compare them to the implementation, and write a report. It came back with a detailed audit document. I cross-checked it. It was accurate. I don’t know whether to be proud or to question my career choices.
Rate limiting is done via WordPress transients — no extra tables needed. By default it’s 5 booking attempts per 10 minutes per IP. There’s also a `WPAPPT_TRUST_PROXY` constant for sites behind a load balancer, because blindly trusting `X-Forwarded-For` without that guard lets anyone rotate fake IPs and blow straight through the rate limiter. The AI caught this one before I did. I’m choosing to frame that as “good tooling” rather than “the robot is better at security than me.”
There’s also a honeypot field in the booking form — a CSS-hidden text input called `website`. The server silently rejects any submission where that field is non-empty. I used `position: absolute; left: -9999px` rather than `display: none` or `type=”hidden”` because some bots specifically skip fields that look hidden. This was the AI’s suggestion. I verified it was correct. It was. The robot 2, me 0.
Testing
The test suite runs via PHPUnit with Brain Monkey and Mockery, so no WordPress install is needed to run it. All critical paths have coverage — booking validation, availability logic, token generation and expiry, rate limiting, double-booking protection, the email service, and the REST API endpoints. 169 tests total at the time of writing.
The AI wrote all the tests as well. Every single one. Which either means vibe-coding produces well-tested software, or the AI is very good at writing tests that confirm its own code works, which is a slightly different thing. Like asking Gollum to guard the One Ring. The tests do pass though. All 169 of them. Every time. On demand. In a Docker container. With Brain Monkey stubbing all the WordPress functions so we don’t need a full WP install.
I checked them. They’re good tests. Probably…
Closing Thoughts
Would I vibe-code again? Absolutely. I’ve been quite productive in a language where I’m not mentally ready to write any code with anymore. The key is to stay engaged — read everything, push back when something feels wrong, and never just blindly accept what the AI produces. Treat it like any other developer who has read every Stack Overflow post ever written but has never actually shipped anything to production.
Also: it has no feelings, so you can tell it its code is bad as many times as you need to. Unlike a real developer. Not that I would know anything about that. :p
The plugin is live and running. Appointments are being booked. Nobody has been double-booked. The Flying Spaghetti Monster has not materialised in anyone’s kitchen. All is well in whatever practice you happen to be running.
*”It’s a dangerous business, going out your front door.”* — Bilbo Baggins, who clearly never tried to build a WordPress plugin without a framework.