Skip to content
Elmarzougui

Moroccan Meraki — Booking Platform for Artist-Led Retreats

A custom Laravel platform replacing WordPress for an artist-led retreat business in Morocco, built around a transactional, concurrency-safe booking engine.

  • Laravel 13
  • Booking engine
  • MySQL concurrency
  • Pest
  • PHP 8.4
  • MySQL
  • Blade
  • Alpine.js
  • Vite
  • Spatie Permission
  • Spatie Model States
  • Spatie Media Library
  • Spatie Query Builder
  • mPDF

Overview

Moroccan Meraki organises artist-led creative retreats in Morocco. International painters and instructors bring their students, and Moroccan Meraki handles everything on the ground: accommodation, transport, itineraries, local suppliers and participant logistics. The promise to instructors is simple: you teach, we handle Morocco.

The business runs on WordPress today. I am designing and building a custom Laravel platform to replace it, covering the full retreat lifecycle from creating a retreat to booking, capacity and operations.

The problem

A retreat isn't a tour package. The instructor is the product, and each retreat combines a creative discipline, destinations, workshops, accommodation, meals and a community of participants. A generic travel or booking plugin can't model that, so the business needed its own engine.

Generic travel site:  Destination → Hotel → Activities → Price → Book
Moroccan Meraki:      Artist → Creative journey → Morocco → Community → Retreat → Booking

Retreats as the central entity

A Retreat holds its instructors, destinations, dates, itinerary, media, inclusions and exclusions, accommodation, participant categories, pricing, capacity, deposits and booking windows. Most of these capabilities are optional, so the platform supports both fully structured commercial retreats and simpler experiences without forcing artificial configuration.

Instructors are first-class actors, not metadata on a package. Each retreat can also define whether customer funds go to Moroccan Meraki or to the instructor, and I kept who receives the money separate from how the payment is processed, so the domain isn't tied to any payment provider.

Accommodation as its own domain

Accommodation is a reusable domain rather than hotel fields on a retreat:

Accommodation → Retreat stay → Allocation → Accommodation option
  • Accommodation: a reusable property.
  • Retreat stay: the use of that property during one retreat.
  • Allocation: the rooms Moroccan Meraki actually controls for that retreat, not the hotel's whole inventory.
  • Accommodation option: what participants choose, such as an included room, a shared room or a private upgrade.

Room inventory is tracked separately from participant capacity. Shared rooms are never assigned to strangers automatically; participants sharing a room must be grouped explicitly.

No fake defaults

A key rule across the commercial configuration: the absence of a setting is never stored as a default. No price doesn't mean free, no deposit setting isn't a stored zero, no participant category doesn't create an artificial "General" one, and no capacity limit doesn't mean infinite.

The same rule applies to money. Unknown is not zero: null means no amount has been agreed yet, and 0 means the amount is known to be zero.

The booking core

Booking is a transactional domain, not a form that inserts a row. A booking holds the booker, one or more participants, optional pricing and accommodation choices, explicit room occupants, attribution, and snapshots of the retreat, instructor, commercial terms and payout recipient.

User ≠ Booker ≠ Participant

The person booking doesn't have to travel, and participants don't need accounts. The engine handles all four combinations of commercial rules and accommodation (both, either, or neither), so an optional feature never becomes an accidental requirement.

  • Integer money: all amounts are stored in minor units (3,500.00 is 350000), never as floats.
  • Server-side pricing: totals, prices, payout routing and snapshots are always computed on the server. A client can't submit its own price.
  • Historical integrity: if a retreat's title, dates, instructor, prices or accommodation change later, existing bookings still show what was actually agreed.
  • A small lifecycle: Pending → Confirmed → Completed, with cancellation from Pending or Confirmed. Payment status is deliberately kept out of the booking state machine, so each state machine owns one concept.

Concurrency and idempotency

The last place on a retreat or the last room can be claimed by two people at the same moment. Confirmation runs inside database transactions with MySQL pessimistic locking, acquiring locks in a deterministic order to avoid deadlocks. Pending bookings reserve nothing; resources are secured only on confirmation.

This behaviour is tested with real MySQL concurrency tests, not only SQLite unit tests.

Submissions are also idempotent. If the same booking is sent twice (a double click, a browser retry or a network failure), the original booking is returned instead of a duplicate. If the same token arrives with a different booking, the conflict is detected. Raw tokens and full request payloads are never stored.

Media library and CMS

I built shared media infrastructure instead of separate uploads per feature:

MediaAsset → AssetUsage → Retreat, itinerary entry, page, post, FAQ…

One canonical asset can be reused anywhere, with folders, ordering, usage tracking and duplicate detection. Removing an image from one place never deletes an asset still used elsewhere.

The CMS follows the same principle, Definition ≠ Usage: hierarchical pages with URL and redirect management, a blog, reusable FAQs, and reusable inclusions and exclusions, each defined once and assigned where needed with its own ordering.

Testing

At the completion of Booking Core V1, the suite had 455 tests and 3,321 assertions with zero failures, including dedicated MySQL concurrency tests for race-sensitive scenarios. Booking alone is covered across submission, lifecycle transitions, integrity constraints, optional configurations, pricing semantics, accommodation rules, idempotency and authorization.

Architecture

The platform is a pragmatic modular monolith:

Controller → Action → Focused service → Domain models (Eloquent)

Controllers stay thin HTTP adapters, Actions are use cases and transaction boundaries, and Services hold reusable domain logic. There are no microservices, repositories or DDD ceremony where Laravel already provides a cleaner answer.

Roadmap: designed, not yet built

  • Payments: a provider-agnostic domain (Booking → Payment layer → Gateway contract → Provider adapter) supporting Stripe, PayPal, CMI, Payzone and offline payments. Payment status, attempts, refunds and reconciliation live there, not in the booking.
  • Holds and waitlist: temporary reservations of places and rooms, and demand capture once a retreat is full, as separate domains with their own lifecycles.
  • Travel information: flights, transfers, dietary needs and emergency contacts collected after booking, with their own privacy and authorization rules.
  • My Retreat: a participant space for bookings, balances, itinerary, documents and updates, possibly with magic-link access instead of mandatory accounts.
  • Retreat operations: rooming lists, manifests, transfers, guides and suppliers, added only where they match a real workflow.
  • Public website: an original design built from the brand's existing visual identity: artistic, editorial, warm and photographic, without tourism clichés.

My role

I lead the software architecture and development of the new platform: domain modelling, the booking and inventory engine, concurrency and idempotency, money and pricing design, accommodation and commercial rules, the media library and CMS, authorization, testing strategy and the admin platform.

Have a proposal?

Let's Start