Book meetings in Laravel with Wireschedule

Sometimes you want your visitors to be able to book a call or a demo with you, and you might not want to pay for Calendy or to use external widgets.

You might also want the booking data in your own database, not leaving for someone else’s dashboard, and you want the booking to actually plug into your app’s pipeline instead of being a dead-end iframe.

Wireschedule is a self-hosted meeting scheduler for Laravel that does exactly that. Availability lives in config, bookings land in your own table, and the UI is a Livewire component you drop in. Here is the fast path to getting it running and wired into your own logic.

How to install

It is a normal Composer package, and the service provider registers its migration for you, so there is nothing to publish just to run it.

composer require edulazaro/wireschedule
php artisan migrate

The only manual step is the stylesheet. Import it into your CSS bundle or the widget shows up unstyled.

@import '../../vendor/edulazaro/wireschedule/resources/css/wireschedule.css';

There is no separate JS build. The Livewire view ships its own inline Alpine, so it uses the Livewire and Alpine you already have. Requirements are PHP 8.2+, Laravel 11+ and Livewire 3.

How to use it

Drop the Blade component wherever people should book. The event you pass is a key you will define in config in a second.


Pick the look once on the root element. Wireschedule shares one theme attribute with the rest of the wire* family, and the CSS ships eleven themes including studio, minimal, brutalist, glass and neon.

 data-wire-theme="studio">

Now shape the demo event. Publish the config and open it up.

php artisan vendor:publish --tag=wireschedule-config

Each event is a weekly availability map plus a few booking rules. The map is keyed by ISO weekday, 1 for Monday through 7 for Sunday, and each day is a list of HH:MM ranges. duration and buffer are minutes, days_ahead is the booking horizon, and min_notice is the minimum minutes ahead someone can book.

'timezone' => 'Europe/Madrid', // organiser zone; availability is read here, stored UTC
'events' => [
    'demo' => [
        'label' => 'Demo',
        'duration' => 30,
        'buffer' => 0,
        'days_ahead' => 21,
        'min_notice' => 120,
        'availability' => [
            1 => [['10:00', '14:00'], ['16:00', '18:00']],
            5 => [['10:00', '14:00']],
        ],
    ],
],

The component renders the available dates, the free slots for the selected date, and a short name and email form, saving a booking on confirm.

Wiring the booking into your pipeline

This is the part that makes it more than a form. After a successful save, the Scheduler dispatches a wireschedule-booked Livewire event carrying the new booking id. Listen for it in a parent Livewire component and fire whatever follow-up you need, like pinging your team channel or syncing a CRM.

use LivewireAttributesOn;

class BookingPage extends Component
{
    #[On('wireschedule-booked')]
    public function onBooked(int $id): void
    {
        // notify your team, push to a CRM, dispatch a job...
    }
}

Because the booking is a row in your database, you read it back with plain Eloquent. The model ships scopes so the common query stays short.

use EduLazaroWireScheduleModelsBooking;

Booking::ofType('demo')->confirmed()->upcoming()->orderBy('starts_at')->get();

If the person was logged in when they booked, Wireschedule captures them automatically through a polymorphic booker relation, so you can attribute the booking to an account with no extra work. For a guest it is just null.

$booking->booker; // the authenticated user who booked, or null

Timezones and double-booking are handled

You do not have to think about either of these, which is half the reason to use it. The visitor’s browser timezone is detected client side and used to display the slots, then everything is persisted in UTC with the visitor’s zone stored on the row, so your availability config stays in your own timezone and the maths just works.

Double-booking is blocked at two layers: a unique (event_type, starts_at) index on the table, plus a re-check that the slot is still free at confirm time, so two concurrent requests cannot grab the same slot.

Good to know

The event prop is locked, and on mount the component runs abort_unless against the event config. If you pass an event key that is not defined under wireschedule.events, the component 404s instead of rendering an empty widget. Just define the event in the config before you point the component at it and you are fine.

👉 Package on Packagist: https://packagist.org/packages/edulazaro/wireschedule
👉 Source on GitHub: https://github.com/edulazaro/wireschedule

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post

Organizational Loyalty

Related Posts