DEV Community

Kuba Brzeziński
Kuba Brzeziński

Posted on

BookEase - booking plugin for WordPress

I got frustrated with $89 WordPress booking plugins — so I built my own

And it actually works. Here's how.


If you've ever tried to add appointment booking to a WordPress site, you know the pain.

Bookly — $89 one-time, or $9.99/month for the full version. Half the features are locked behind add-ons that cost extra.

Simply Schedule Appointments — free version is crippled. Pro starts at $99/year.

Amelia — $79/year. Nice UI, but massive plugin that loads scripts on every page.

For a barber, a personal trainer, or a small clinic — this is just too much. They want to accept bookings online. They don't want to pay a monthly subscription forever.

So I built BookEase — a lightweight WordPress booking plugin. One shortcode. No monthly fees. Pay once.

Here's how I built it, what I learned, and what tripped me up.


The goal

I wanted to build something that:

  • Works with [bookease_form] on any page
  • Guides clients through 4 steps: Service → Date & Time → Details → Confirm
  • Sends email confirmations automatically
  • Has a clean admin panel to manage bookings
  • Is written properly — PHP 8.1+, PSR-4, no jQuery spaghetti

Architecture

I went with the classic WordPress plugin OOP structure:

bookease/
├── bookease.php              # Main plugin file
├── includes/
│   ├── class-bookease.php    # Core orchestrator
│   ├── class-loader.php      # Action/filter queue
│   ├── class-activator.php   # DB tables on activation
│   └── class-bookease-emails.php
├── admin/
│   ├── class-bookease-admin.php
│   └── partials/             # Dashboard, bookings, services, settings
├── public/
│   ├── class-bookease-public.php
│   ├── js/public.js
│   └── partials/booking-form.php
└── languages/
    └── bookease.pot
Enter fullscreen mode Exit fullscreen mode

The class-loader.php pattern keeps hooks clean — you queue actions and filters, then run them all at once. No scattered add_action() calls everywhere.


The database

Three tables, created with dbDelta() on activation:

$sql = "CREATE TABLE {$wpdb->prefix}bookease_services (
    id bigint(20) NOT NULL AUTO_INCREMENT,
    name varchar(255) NOT NULL,
    duration int(11) NOT NULL DEFAULT 60,
    price decimal(10,2) NOT NULL DEFAULT 0.00,
    description text,
    created_at datetime DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id)
) $charset_collate;";
Enter fullscreen mode Exit fullscreen mode

dbDelta() is the WordPress-safe way to create/upgrade tables. It only applies changes — so activating the plugin on an existing install won't destroy data.


The slot engine — the tricky part

This is where most booking plugins get it wrong. You can't just divide working hours by service duration.

You need to account for:

  1. Service duration (how long the appointment takes)
  2. Buffer time between appointments
  3. Existing bookings (no double-booking)
  4. Past time slots (don't show slots that already passed today)

My approach:

private function get_available_slots( $date, $service_id ) {
    $service  = $this->get_service( $service_id );
    $duration = (int) $service->duration;
    $break    = (int) get_option( 'bookease_break_between', 0 );
    $hours    = $this->get_working_hours( $date );

    if ( ! $hours['open'] ) return [];

    $slots    = [];
    $current  = strtotime( $date . ' ' . $hours['from'] );
    $end      = strtotime( $date . ' ' . $hours['to'] );
    $now      = time() + 3600; // 1h buffer for same-day bookings

    while ( $current + ( $duration * 60 ) <= $end ) {
        $slot_end = $current + ( $duration * 60 );

        if ( $current >= $now && ! $this->is_slot_taken( $current, $slot_end, $date ) ) {
            $slots[] = date( 'H:i', $current );
        }

        $current += ( ( $duration + $break ) * 60 );
    }

    return $slots;
}
Enter fullscreen mode Exit fullscreen mode

The is_slot_taken() check queries the DB for any confirmed/pending booking that overlaps the proposed slot. Simple but effective.


The frontend — AJAX 4-step form

No page reloads. State lives in a JS object:

const state = {
    step: 1,
    serviceId: null,
    serviceName: null,
    date: null,
    time: null,
    customerName: null,
    customerEmail: null,
    customerPhone: null,
    notes: null
};
Enter fullscreen mode Exit fullscreen mode

Each step validates before moving to the next. On step 2, picking a date fires an AJAX call to get available slots:

async function fetchAvailableSlots( date ) {
    const response = await fetch( bookease.ajax_url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams({
            action: 'bookease_get_available_times',
            nonce:  bookease.nonce,
            date:   date,
            service_id: state.serviceId
        })
    });
    const data = await response.json();
    renderTimeSlots( data.slots );
}
Enter fullscreen mode Exit fullscreen mode

Nonce verification on every AJAX handler — non-negotiable.


Email notifications

WordPress's wp_mail() is fine for most use cases. I built HTML templates with inline CSS (because email clients are a nightmare):

public function send_customer_confirmation( $booking_id ) {
    $booking = $this->get_booking( $booking_id );
    $to      = $booking->customer_email;
    $subject = sprintf( '[%s] Your Booking is Confirmed', get_option('bookease_business_name') );

    $headers = ['Content-Type: text/html; charset=UTF-8'];
    $message = $this->get_confirmation_template( $booking );

    wp_mail( $to, $subject, $message, $headers );
}
Enter fullscreen mode Exit fullscreen mode

Compatible with any SMTP plugin (WP Mail SMTP, FluentSMTP) since it hooks into wp_mail.


Security checklist

Every form and AJAX handler has:

  • wp_verify_nonce() — prevents CSRF
  • sanitize_text_field() / sanitize_email() — cleans input
  • $wpdb->prepare() — prevents SQL injection
  • current_user_can() — capability checks in admin

This is table stakes for any WordPress plugin. Don't skip it.


What I'd do differently

1. Add Google Calendar sync from day one.
It's the most requested feature by anyone running a service business. I'll add it in v1.1.

2. Better error handling on the frontend.
Right now errors are generic. Should be more specific ("That slot was just taken, please pick another").

3. Consider a REST API instead of admin-ajax.
admin-ajax.php works but the WP REST API is cleaner and easier to test.


The result

A working WordPress booking plugin. 4-step form, smart availability engine, email notifications, clean admin panel. PHP 8.1+, PSR-4, ~44KB.

👉 kuba07.gumroad.com/l/bookease


Takeaways

Building a WordPress plugin from scratch taught me a lot about the ecosystem — hooks, wpdb, nonces, capability checks. It's a different world from modern frameworks, but the fundamentals (clean architecture, security, separation of concerns) are the same everywhere.

If you're a developer looking to build passive income, WordPress plugins are still a solid bet. The market is huge, the bar for quality is surprisingly low, and a well-built plugin can sell for years.

Happy to answer questions in the comments!


Tags: #wordpress #php #webdev #sideproject

Top comments (0)