HomeAboutServicesPortfolioSEOContactBook Free Strategy Call
Portfolio / The Cupping Guy / How We Built It
Deep Dive PHP 8 MySQL Custom Build

How We Built
The Cupping Guy

A complete technical breakdown — from the first wireframe to the live deployment. This page covers how 9WEB designed and coded the public-facing website and the fully bespoke backend booking portal for a London Hijama and therapy practice. Every decision, every line of logic, explained.

thecuppingguy.com — Public Website
portal.thecuppingguy.com — Booking Portal
Book Appointment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Available slots — Today
9am
11am
2pm
4pm
Chapter 01

The Brief — What We Were Asked to Build

The Cupping Guy is a specialist Hijama (wet cupping) and muscular therapy practice based in London. When 9WEB was brought on, the client had no website and no digital infrastructure at all. Appointments were managed manually through WhatsApp and phone calls — which worked when the client had a handful of patients, but quickly became unmanageable as the practice grew.

The brief had two distinct parts:

🌐
Part 1 — Public Website
  • A professional, mobile-first website introducing the practice
  • Service pages for each treatment offered
  • About section with practitioner credentials
  • Clear call-to-action driving visitors to book
  • London local SEO — ranking in the map pack for Hijama searches
⚙️
Part 2 — Booking Portal
  • A client-facing booking system where patients self-serve
  • Secure client accounts with appointment history
  • Real-time availability — no double bookings
  • Automated email confirmations and reminders
  • A private admin dashboard for the practitioner

Off-the-shelf solutions like Calendly, Acuity or Squarespace bookings were considered and rejected. They either lacked the custom client record management needed, added unwanted branding, or couldn't integrate with a dedicated client portal. The decision was made to build everything bespoke.

Key constraint: The entire platform had to live on a single shared hosting environment (Hostinger) — meaning no Docker, no Node.js, no complex infrastructure. The stack had to work with PHP + MySQL + Apache, which actually suited the project perfectly.

Chapter 02

Website Design — Look, Feel & Structure

The design brief for the public website was: trustworthy, clean, and health-focused. Hijama is a traditional Islamic and holistic therapy — the design needed to feel calm and professional, not clinical. Patients needed to feel they were in safe, expert hands before they'd even picked up the phone.

Colour palette: Deep forest green as the primary brand colour, paired with white and a warm off-white. Green was chosen deliberately — it signals healing, nature and wellbeing without the starkness of a medical white. A teal accent was used for calls-to-action to create contrast and guide the eye toward booking buttons.

Typography: A clean sans-serif (Plus Jakarta Sans) throughout — readable at all sizes, professional, never cold. Headers carry appropriate weight without feeling aggressive.

Page structure built:

  • Homepage — hero with a clear headline and CTA, services overview, about snippet, trust signals (years of experience, treatments delivered), and a direct link to the booking portal
  • Services pages — individual pages for Hijama (Wet Cupping), Fire Cupping, Deep Tissue Massage, Spinal Alignment, Sports Massage, Posture Correction and Detox. Each with treatment description, what to expect, preparation advice and a booking CTA
  • About page — practitioner background, qualifications, why Hijama, Islamic context of the practice
  • Contact page — location map, phone, email and a link to the portal

Mobile-first approach: The majority of health and therapy searches happen on mobile. Every page was designed mobile-first — the desktop layout is an enhancement, not the primary focus. Touch targets, readable font sizes and fast-loading images were non-negotiable.

SEO decision: Each treatment got its own dedicated URL (e.g. /services/hijama-london, /services/fire-cupping-london). This meant each page could rank individually for its own treatment-specific search terms — rather than burying everything on a single services page which would dilute ranking potential.

Chapter 03

Frontend Code — HTML, CSS & JavaScript

The website frontend was built in clean, semantic HTML5 with custom CSS3 — no Bootstrap, no heavy UI framework. This kept the site fast (critical for Google Core Web Vitals) and gave us full control over every pixel.

Why no framework? Bootstrap and Tailwind are excellent tools, but they add overhead. For a focused 6-page site, a purpose-built stylesheet is lighter, faster and simpler to maintain. The CSS was structured using custom properties (variables) for the colour palette and spacing system, making global changes trivial.

CSS
/* Brand variables — change here, updates everywhere */
:root {
  --green-primary:  #1a6b35;
  --green-dark:     #0f3a1f;
  --teal-accent:    #00bfa6;
  --white:          #ffffff;
  --off-white:      #f7f9f4;
  --font-body:      'Plus Jakarta Sans', sans-serif;
  --radius:         10px;
  --shadow:         0 8px 32px rgba(15, 58, 31, 0.12);
}

JavaScript: Vanilla JS only — no jQuery, no React. The scripts handled mobile navigation toggling, scroll-triggered animations using the Intersection Observer API, smooth anchor scrolling and form validation on the contact page.

JavaScript
// Reveal elements as they scroll into view
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add('visible');
      observer.unobserve(entry.target);
    }
  });
}, { threshold: 0.12 });

document.querySelectorAll('.reveal').forEach(el => {
  observer.observe(el);
});

Performance: Images were compressed and served in WebP format where supported. Fonts were loaded with display=swap to prevent invisible text during load. The site consistently scores 90+ on Google PageSpeed Insights — which directly impacts search rankings.

Chapter 04

Database Design — The MySQL Schema

The booking portal is backed by a relational MySQL database. Getting the schema right at the start is critical — a poorly structured database creates problems that compound over time. We mapped out all the entities, their relationships and their constraints before writing a single line of PHP.

The database has five core tables:

Database Schema — Core Tables
clients
id INT · PK
first_name VARCHAR
last_name VARCHAR
email VARCHAR · UNIQUE
phone VARCHAR
password_hash VARCHAR
health_notes TEXT
created_at TIMESTAMP
appointments
id INT · PK
client_id FK → clients
service_id FK → services
appt_date DATE
appt_time TIME
status ENUM
practitioner_notes TEXT
booked_at TIMESTAMP
services
id INT · PK
name VARCHAR
description TEXT
duration_mins INT
price_gbp DECIMAL
is_active TINYINT
availability
id INT · PK
day_of_week TINYINT
start_time TIME
end_time TIME
is_active TINYINT
blocked_dates
id INT · PK
blocked_date DATE
reason VARCHAR
created_at TIMESTAMP

The appointments table is the heart of the system. Its status column uses an ENUM — 'pending', 'confirmed', 'cancelled', 'completed' — which constrains the value at database level, not just application level. This prevents dirty data from ever entering the system.

Foreign key constraints between appointments → clients and appointments → services ensure referential integrity — you can never have a booking for a non-existent client or a deleted service.

Key design decision: Working hours are stored in availability by day-of-week (0=Monday, 6=Sunday) rather than hardcoded. This means the practitioner can change their working hours from the admin panel without any code changes.

Chapter 05

Backend API — PHP 8 Endpoints

The backend is built in PHP 8 following a clean endpoint-per-action pattern. Rather than a heavy MVC framework, we used a lightweight router that maps URL paths to handler functions — keeping the codebase minimal and easy to reason about.

All database access uses PDO with prepared statements. This is non-negotiable — it is the only correct way to interact with a MySQL database in PHP. String concatenation in SQL queries creates SQL injection vulnerabilities; prepared statements make that structurally impossible.

PHP
// ✅ Correct — prepared statement, SQL injection impossible
function getClientByEmail($pdo, $email): array|false {
    $stmt = $pdo->prepare(
        'SELECT * FROM clients WHERE email = :email LIMIT 1'
    );
    $stmt->execute(['email' => $email]);
    return $stmt->fetch(PDO::FETCH_ASSOC);
}

// ❌ Wrong — NEVER do this (SQL injection risk)
// $result = $pdo->query("SELECT * FROM clients WHERE email = '$email'");

The core API endpoints built:

1

POST /api/register

Creates a new client account. Validates all fields server-side, checks the email isn't already registered, hashes the password with password_hash($pw, PASSWORD_BCRYPT), inserts the record and returns a session token.

2

POST /api/login

Fetches the client record by email, verifies the password with password_verify(), regenerates the session ID with session_regenerate_id(true) to prevent session fixation, then stores the client ID in the session.

3

GET /api/availability?date=YYYY-MM-DD&service_id=N

The most complex endpoint. Fetches the practitioner's working hours for that day of week, generates all possible slots based on service duration, then subtracts any slots already booked that day and any blocked dates. Returns an array of available time strings.

4

POST /api/book

Validates the client is logged in, re-checks the slot is still available (to handle race conditions between two clients booking simultaneously), creates the appointment record with status 'confirmed', then triggers the email confirmation job.

5

POST /api/cancel

Checks the appointment belongs to the requesting client, verifies it's more than 24 hours away (business rule), updates status to 'cancelled' and fires a cancellation notification to both client and practitioner.

6

GET /api/my-appointments

Returns all upcoming and past appointments for the authenticated client, joined with service names and formatted dates. Results are ordered by appointment date descending — most recent first.

Chapter 06

The Booking Logic — How Slot Generation Works

The availability engine is the most technically interesting part of the system. It needs to answer a simple question — "what times are free on this date for this service?" — but doing so correctly requires several steps.

PHP — Availability Engine
function getAvailableSlots($pdo, $date, $service_id): array {

    // 1. Is this date blocked entirely?
    $blocked = isDateBlocked($pdo, $date);
    if ($blocked) return [];

    // 2. Get working hours for this day of week
    $dayOfWeek = (int) date('N', strtotime($date)) - 1;
    $hours = getWorkingHours($pdo, $dayOfWeek);
    if (!$hours) return []; // day off

    // 3. Get service duration
    $duration = getServiceDuration($pdo, $service_id);

    // 4. Generate all possible slots
    $allSlots = generateSlots(
        $hours['start_time'],
        $hours['end_time'],
        $duration
    );

    // 5. Remove already-booked slots
    $booked = getBookedTimes($pdo, $date);
    return array_diff($allSlots, $booked);
}

Handling race conditions: Two clients could theoretically see the same slot available at the same moment and both try to book it. To handle this, the final booking write uses a database transaction with a SELECT ... FOR UPDATE lock on the relevant time slot row. If both requests arrive simultaneously, the database serialises them — the first commits successfully, the second gets a conflict error and the user sees a "sorry, that slot was just taken" message.

Duration-aware slots: A 30-minute Hijama session generates slots at 09:00, 09:30, 10:00 etc. A 60-minute deep tissue massage generates slots at 09:00, 10:00, 11:00. The slot generator respects the service duration — a 60-minute booking at 09:00 blocks 09:00 AND prevents anything starting at 09:30 (which would overlap). This is calculated by comparing booked slot start times against the full duration window, not just the start time.

Chapter 07

Authentication & Security

Any system handling personal health data and client records has a serious obligation to be secure. We took a layered approach — multiple independent security controls so that if one fails, others still protect the system.

🔐
bcrypt Password Hashing
All passwords are hashed using PHP's password_hash() with PASSWORD_BCRYPT. Plain text passwords are never stored anywhere — not in the database, not in logs, not in emails.
🛡️
PDO Prepared Statements
Every database query uses PDO with bound parameters. SQL injection attacks are structurally impossible — user input is never concatenated into SQL strings.
🔄
Session Regeneration
On every successful login, session_regenerate_id(true) is called to issue a new session ID. This defeats session fixation attacks where an attacker pre-sets a known session ID.
🧹
Input Sanitisation
All user input is sanitised with htmlspecialchars() on output and validated server-side on input. Client-side validation is a UX feature — server-side validation is the actual security control.
🔒
SSL Everywhere
The entire portal runs over HTTPS. The server is configured to redirect all HTTP traffic to HTTPS and the HSTS header is set to prevent downgrade attacks.
🚧
CSRF Protection
All state-changing requests (booking, cancellation, profile updates) require a CSRF token — a unique, session-bound value generated server-side and validated on every POST request.
🔑
Admin Route Isolation
The admin panel lives on a separate route with its own authentication check. Regular client sessions have zero access to admin functions — the authorisation check happens at the PHP level, not just in the UI.
📋
Ownership Verification
Every request that reads or modifies an appointment checks that the appointment's client_id matches the authenticated session user. Client A cannot read, cancel or modify Client B's bookings.
Chapter 08

Email Automation — Confirmations & Reminders

Automated emails are a core feature of the booking experience. Clients expect an immediate confirmation when they book, and a reminder before their appointment. We built this using PHPMailer — the industry standard PHP email library — sending via a dedicated SMTP relay for reliable delivery.

Why not PHP's built-in mail() function? Because it's unreliable on shared hosting, offers no authentication, and gets flagged as spam far more often. PHPMailer with SMTP authentication gives us proper delivery, TLS encryption, and bounce handling.

PHP — Email Trigger
function sendBookingConfirmation($client, $appointment, $service): bool {
    $mail = new PHPMailer(true);
    $mail->isSMTP();
    $mail->SMTPAuth   = true;
    $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
    $mail->Username   = getenv('SMTP_USER');  // from .env
    $mail->Password   = getenv('SMTP_PASS');  // never hardcoded

    $mail->addAddress($client['email'], $client['first_name']);
    $mail->addBCC('bookings@thecuppingguy.com'); // practitioner copy
    $mail->Subject = 'Your appointment is confirmed — The Cupping Guy';
    $mail->isHTML(true);
    $mail->Body    = renderEmailTemplate('confirmation', [
        'name'    => $client['first_name'],
        'service' => $service['name'],
        'date'    => formatDate($appointment['appt_date']),
        'time'    => $appointment['appt_time'],
    ]);
    return $mail->send();
}

SMTP credentials security: Notice the credentials are fetched from getenv() — they live in a .env file on the server, outside the web root, and are never committed to any repository. This is standard practice and means credentials are never exposed even if source code is somehow leaked.

Three automated emails are sent:

  1. Booking confirmation — triggered immediately on successful booking. Contains service name, date, time, location details and preparation notes specific to the treatment booked.
  2. 24-hour reminder — sent the day before the appointment. This runs via a scheduled task (cron job) that queries for all appointments happening tomorrow morning and emails each client.
  3. Cancellation confirmation — triggered when a client or the practitioner cancels. Both parties are notified with the next-steps message.
Chapter 09

The Admin Panel — What the Practitioner Sees

The admin panel is the practitioner's command centre. It lives at /admin — a route that checks for an admin session on every request. No admin session means an immediate redirect to the admin login page, with no information about what's behind the route.

Admin capabilities built:

  • Dashboard overview — today's appointments at a glance, upcoming bookings this week, total clients registered, recent cancellations
  • Calendar view — monthly calendar showing booked, available and blocked days. Click any day to see that day's appointments in full detail
  • Appointment management — view, confirm, complete or cancel any appointment. Add private practitioner notes visible only in the admin panel
  • Client directory — searchable list of all registered clients with full contact details, health intake notes and complete appointment history per client
  • Availability management — set working hours per day of week. Toggle days on/off. Add blocked dates (holidays, training days) with a reason note
  • Service management — add, edit or deactivate services. Update prices, descriptions and durations. Deactivated services stop appearing in the booking flow without deleting historical data

Soft deletes everywhere: Nothing in the admin panel hard-deletes data. Services are deactivated, not deleted. Appointments are cancelled with a timestamp, not removed. This preserves historical records — critical for a health practice that may need to reference past treatments.

Chapter 10

Deployment — Taking It Live

The platform is hosted on Hostinger shared hosting — deliberately. The client didn't need a VPS or cloud server; shared hosting with PHP 8 and MySQL is perfectly capable of handling this traffic volume, costs a fraction of the price, and is far simpler to manage.

Before going live, a deployment checklist was worked through:

SSL certificate installed and all HTTP traffic redirected to HTTPS via .htaccess
Environment variables moved to .env outside web root — no credentials in code
PHP error display turned off in production (display_errors = Off) — errors log to file, never shown to users
Database backups configured — automated daily MySQL dump via Hostinger's backup tool
Cron job set up for the 24-hour reminder email — runs at 8am daily
All user journeys tested end-to-end — registration, booking, cancellation, rescheduling, admin login, email delivery
Google Search Console connected and sitemap submitted for the public website
Google Analytics 4 installed on the public website with conversion tracking on booking CTA clicks
Admin panel walkthrough delivered to the practitioner — how to manage bookings, set availability and add blocked dates

The full project — website design, frontend code, database design, backend API, admin panel, email system and deployment — was completed and delivered by 9WEB. The platform has been running reliably since launch with zero unplanned downtime.

Want something similar built for your business? Whether it's a booking system, a client portal, a membership platform or bespoke business software — 9WEB can scope, design and build it. Book a free 30-minute call and we'll tell you exactly how we'd approach your project.

Need bespoke software built for your business?

9WEB designs and builds custom web applications, booking systems and client portals — tailored precisely to your workflow. Book a free call and let's talk through your requirements.