Skip to content

JOURNAL // ARCHIVE_ENTRY

What Changed in Bookable Calendar 3.0 and 3.1

I pushed two new development lines for Bookable Calendar this week. Version 3.0 is the largest rewrite since I first released the module in 2020, and 3.1 builds payments and two-way calendar availability on top of that foundation.

The current tags are 3.0.0-rc2 and 3.1.0-alpha2. They are prereleases, so the stable release shown on Drupal.org is still 2.2.13. Here is what changed and why.

One safe path for every reservation

The biggest 3.0 change is internal but solves a very visible problem: every way to create or change a reservation now follows the same rules.

Bookable Calendar has accumulated several entry points over the years. A reservation can come from the normal form, one-click booking, a custom API endpoint, JSON:API, a check-in screen, a delete form, or a Views Bulk Operations action. When those paths each handle capacity and validation separately, they can drift. More importantly, two requests can both see the last available slot before either one finishes saving.

Version 3 routes those writes through a transactional reservation manager. It locks every affected opening instance, starts a database transaction, and rechecks access, timing, and capacity inside that transaction. A multi-instance request saves everything or rolls everything back. Reservation events fire only after the transaction commits.

That shared boundary prevents concurrent requests from overbooking an opening and gives forms, APIs, and integrations the same behavior.

A smaller reservation data model

Version 2 stored a Booking Contact plus separate booking rows for attendees. Version 3 consolidates each customer reservation into the Booking Contact entity and its party_size. It also removes stored reverse-reference fields between calendars, openings, instances, and reservations.

The result has fewer entities to synchronize and fewer ways for references to disagree. Opening definitions now come from the opening-to-calendar reference, and a dedicated synchronizer generates opening instances.

This is a destructive data-model upgrade. Existing sites need the latest 2.2.x database updates first, and custom fields on the removed booking entity need a site-specific migration. The update can consolidate standard attendee rows into party sizes, but it cannot guess how custom attendee data should be combined. I wrote a detailed upgrade guide with requirements, checks, verification steps, and rollback instructions.

Consistent APIs and tighter access

JSON:API create, read, update, and delete operations now use the same reservation manager as Drupal's forms. Custom API endpoints remain available for existing integrations, but cookie-authenticated writes require CSRF tokens.

Clients cannot assign reservation ownership or set notification-only fields. Anonymous edit links now use scoped access tokens instead of predictable values. Google and Microsoft credentials live in encrypted runtime storage rather than exported configuration.

These changes make the API more useful because it no longer creates a second class of reservation that can bypass the normal capacity or ownership rules.

Notifications and frontend components

The core module queues confirmation and cancellation notifications after a successful transaction, deduplicates delivery, supports HTML templates and booking tokens, renders for the recipient's language, and includes a preview screen. ECA was only ever used for time-based follow-ups and reminders, not for confirmation or cancellation notifications.

Version 3 also ships schema-backed Single Directory Components for availability, booking actions, and daily opening groups. Themes can override those components while keeping the same prop contract, which is much cleaner than copying a pile of module templates and CSS.

Google Calendar and Microsoft Outlook

Version 3 adds optional Google and Microsoft provider modules. Each Bookable Calendar can point to a different external destination, and each reservation becomes its own private event. The customer is the only invited attendee, so one customer never sees another customer's email address. Updates and cancellations modify the existing event instead of creating duplicates.

The providers handle OAuth, token refresh, per-calendar destinations, free/busy requests, reconciliation, and sync status. External work runs through queues so a slow provider does not hold open the reservation request.

Version 3.1 blocks conflicts in both directions

Outbound event sync answers one half of the calendar problem: a reservation made in Drupal appears in Google Calendar or Outlook. Version 3.1 adds the reverse direction for availability.

Each calendar now has separate controls for outbound reservation events and inbound busy-time blocking. When inbound sync is enabled, cron checks future opening instances against unrelated busy events. Conflicting openings are hidden temporarily. If the meeting moves or disappears, the openings become available again. Existing reservations and opening instances are never deleted, and events created by Bookable Calendar are ignored so the integration cannot block itself.

This makes an external work calendar useful as a real availability source without turning it into the system of record for Bookable Calendar.

Paid reservations with Drupal Commerce

The other 3.1 addition is the optional bookable_calendar_commerce submodule for Drupal Commerce 3.x. A calendar can use a published product variation as its price and charge once per reservation or once per attendee.

Checkout creates a pending_payment reservation hold that counts against capacity. Each reservation gets its own order item. The reservation confirms only after Commerce reports that the order is fully paid. Removing the order item, cancelling the order, or letting checkout expire releases the held capacity.

Priced calendars reject direct API placement and ignore one-click booking, which prevents either path from bypassing payment. Cron clears expired holds, and reservation-specific locks serialize payment and cancellation updates.

A stricter release process

The rewrite also adds a much larger automated test suite and release pipeline. It exercises the reservation lifecycle, concurrent capacity checks, APIs, installation and upgrade paths, administration screens, Google and Microsoft providers, external availability reconciliation, Commerce checkout, and optional integrations.

The release checks run Composer validation, spelling, JavaScript and CSS linting, Drupal coding standards, static analysis, Upgrade Status, secret detection, PHPUnit, and coverage checks across supported Drupal variants.

Trying the new versions

Bookable Calendar 3 requires Drupal 11 or 12, PHP 8.3 or newer with Sodium, and Smart Date 4.3 or newer. Because the upgrade changes entity storage, test it against a fresh copy of production and back up the database, files, configuration, and code together.

You can inspect the 3.0 branch and the 3.1 branch now. The rewrite touches almost every layer of the module, but the goal remains the same as the first release: make it straightforward for people to publish available time and let someone book it.