Skip to main content
RapidDev - Software Development Agency
weweb-tutorial

WeWeb Events Calendar: Built-in Component and Custom Booking

WeWeb includes a Calendar component (Vue Cal-based) and a Date Picker (VueDatePicker) out of the box. Connect them to Supabase to build a full booking system: fetch available time slots from your backend, let users pick a slot, create a booking record, optionally charge with Stripe, and send a confirmation. No calendar library installation needed.

What you'll learn

  • How to configure the WeWeb Calendar component (views, event binding, click handlers)
  • How to fetch and display available time slots from a Supabase bookings table
  • How to build a multi-step booking flow: slot selection, user details form, payment, confirmation
  • How to create, update, and cancel booking records using Supabase workflow actions
  • How to differentiate event types visually using CSS classes and color-coding
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate11 min read75-90 minWeWeb Free plan and above; Stripe payment step requires Stripe plugin (Essential+)March 2026RapidDev Engineering Team
TL;DR

WeWeb includes a Calendar component (Vue Cal-based) and a Date Picker (VueDatePicker) out of the box. Connect them to Supabase to build a full booking system: fetch available time slots from your backend, let users pick a slot, create a booking record, optionally charge with Stripe, and send a confirmation. No calendar library installation needed.

Build an Events Calendar and Booking System in WeWeb

WeWeb ships with two purpose-built scheduling components: the Calendar (based on Vue Cal) supports year, month, week, and day views with drag-and-drop events, and the Date Picker (based on VueDatePicker) supports single dates, date ranges, and multi-date selection with timezone awareness. This tutorial covers the practical path from zero to a working booking system: designing your Supabase database schema for bookings, fetching events into the Calendar component, handling user slot selection, building the booking form with validation, processing payment via Stripe, and confirming the reservation. You will also learn how to handle recurring events, blocked-out dates, and calendar event color-coding.

Prerequisites

  • A WeWeb project with the Supabase plugin installed and connected
  • A Supabase project with at least a 'bookings' table (created in Step 1)
  • Basic familiarity with WeWeb workflows and the Data panel
  • Stripe plugin installed if adding payment (Step 6)

Step-by-step guide

1

Design the Supabase Bookings Database Schema

This step happens entirely in the Supabase Dashboard, not in WeWeb. Open your Supabase project → Table Editor → New table. Create a table named 'bookings' with these columns: id (uuid, primary key, default: gen_random_uuid()), service_id (uuid, foreign key to a services table), provider_id (uuid, foreign key to users/providers), customer_id (uuid, nullable, foreign key to auth.users), start_time (timestamptz), end_time (timestamptz), status (text, default: 'available' — values: 'available', 'booked', 'cancelled', 'blocked'), customer_name (text, nullable), customer_email (text, nullable), stripe_payment_id (text, nullable), notes (text, nullable), created_at (timestamptz, default: now()). Add an RLS policy so customers can only read 'available' slots and their own bookings. Enable Row Level Security on the table.

typescript
1-- Injection point: Supabase Dashboard SQL Editor
2-- Creates bookings table and RLS policies
3
4CREATE TABLE public.bookings (
5 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
6 service_id UUID,
7 provider_id UUID,
8 customer_id UUID REFERENCES auth.users(id),
9 start_time TIMESTAMPTZ NOT NULL,
10 end_time TIMESTAMPTZ NOT NULL,
11 status TEXT NOT NULL DEFAULT 'available'
12 CHECK (status IN ('available', 'booked', 'cancelled', 'blocked')),
13 customer_name TEXT,
14 customer_email TEXT,
15 stripe_payment_id TEXT,
16 notes TEXT,
17 created_at TIMESTAMPTZ DEFAULT NOW()
18);
19
20ALTER TABLE public.bookings ENABLE ROW LEVEL SECURITY;
21
22-- Anyone can view available slots
23CREATE POLICY "Public can view available bookings"
24ON public.bookings FOR SELECT
25USING (status = 'available');
26
27-- Authenticated users can view their own bookings
28CREATE POLICY "Customers can view own bookings"
29ON public.bookings FOR SELECT
30USING ((SELECT auth.uid()) = customer_id);
31
32-- Authenticated users can update their own available bookings to booked
33CREATE POLICY "Customers can book available slots"
34ON public.bookings FOR UPDATE
35USING (status = 'available')
36WITH CHECK (status = 'booked' AND (SELECT auth.uid()) = customer_id);

Expected result: The bookings table exists in Supabase with RLS enabled and policies applied. Seed it with a few test 'available' slots.

2

Create the Bookings Collection in WeWeb

In the WeWeb editor, open the Data panel → click + New → select Supabase → choose your bookings table. Name the collection 'bookings'. In the Filter section, add a filter: status → equals → 'available'. This ensures only open slots are fetched by default (your Realtime subscription will also catch updates). Set the fetch mode to Dynamic. Add a second collection named 'myBookings' using the same bookings table but with a different filter: customer_id → equals → bind to the logged-in user's ID (if using Supabase Auth, this is: plugins['supabase-auth'].user.id). The Calendar component will use the 'bookings' collection for displaying available slots and 'myBookings' to show the current user's reservations in a different color.

Expected result: Two collections in the Data panel: 'bookings' (available slots) and 'myBookings' (current user's reservations).

3

Add and Configure the Calendar Component

Click the Add panel (+) in the left sidebar, search for 'Calendar', and drag it onto your page. Select the Calendar element and open its Settings panel. Under Data, click the plug icon (🔌) next to 'Events' and bind it to a formula that combines both collections and maps them to the Calendar's expected event format. The Calendar expects each event to have a 'start' (ISO datetime string), 'end' (ISO datetime string), 'title' (display text), and optionally 'class' (CSS class for color-coding). In the Settings panel, set the default view to 'month' or 'week'. Enable the display of all-day events if needed. Under Configuration, set 'min date' to today to prevent past slot selection. The Calendar component supports year, month, week, and day views — add view toggle buttons to the page that use a Change variable value action to update a 'calendarView' variable bound to the Calendar's active view property.

typescript
1// Injection point: Calendar element → Settings → Events property → Formula (plug icon)
2// Maps both collections to Calendar event format
3const available = map(collections['bookings'].data, slot => ({
4 start: slot.start_time,
5 end: slot.end_time,
6 title: 'Available',
7 class: 'slot-available',
8 id: slot.id
9}));
10
11const myBookings = map(collections['myBookings'].data, booking => ({
12 start: booking.start_time,
13 end: booking.end_time,
14 title: booking.notes || 'My Booking',
15 class: 'slot-booked',
16 id: booking.id
17}));
18
19return concat(available, myBookings);

Expected result: Calendar renders with available slots in green and existing user bookings in purple, in month view.

4

Handle Slot Selection and Open the Booking Popup

The Calendar emits an event when a user clicks on a slot. In the Calendar's Settings panel, scroll to Events → On event click. Add a workflow here: Action 1 → Change variable value → set a page variable named 'selectedSlot' (Object type) to the formula: event (the clicked event object, which includes the slot id, start, end). Action 2 → Open popup → select a 'Booking Popup' that you create now. To create the popup: in the Add panel, add a Dialog element, name it 'Booking Popup', and set it to not open on load. Inside the popup, add a text element showing the selected date/time (bound to selectedSlot.start, formatted using a date formula), a Form Container with fields for customer name and email (if not logged in), and a Confirm Booking button. Set the Dialog's close trigger to a button with an On click workflow: Close popup.

Expected result: Clicking an available slot opens a popup showing the selected time and a booking form.

5

Submit the Booking to Supabase

In the Confirm Booking button's On click workflow, add these actions in sequence. Action 1 → Supabase Database Update → table: bookings → set status to 'booked', customer_id to the logged-in user ID (or null for guest), customer_name and customer_email to the form values, updated_at to now() → where: id equals selectedSlot.id. This atomically claims the slot. Action 2 → Fetch collection → 'bookings' (to remove the claimed slot from the available list). Action 3 → Fetch collection → 'myBookings' (to show the new booking in the calendar). Action 4 → Close popup. Action 5 → Change variable value → set a 'bookingConfirmed' boolean to true to show a success banner. Add error handling: use the On error trigger on the workflow to detect if the slot was already booked by another user (Supabase UPDATE will return 0 rows if status is no longer 'available') and show an error message.

Expected result: Clicking Confirm Booking updates the record in Supabase, refreshes collections, closes the popup, and shows a success state.

6

Add Stripe Payment Before Confirming (Optional)

If your booking requires payment, insert payment steps before the Supabase update. In the Confirm Booking workflow, restructure as: Action 1 → Stripe Checkout → redirect to a Stripe-hosted checkout page. Configure the Checkout action in the Plugins → Extensions → Stripe settings with your Price ID. After successful payment, Stripe redirects to a success URL — create a page named 'booking-success' in WeWeb and set it as the success URL. On that page's On page load workflow: read the URL query parameter 'session_id' (add a Query variable named 'session_id' in the Data panel → Variables → New → Query), then call a Supabase Edge Function to verify the payment and update the booking status. In Supabase Dashboard → Edge Functions → New Function: write a Deno function that calls Stripe's retrieve session API and updates the booking record server-side.

Expected result: Users are redirected to Stripe Checkout, and successful payment triggers a server-side booking confirmation.

7

Display the Date Picker for Filtered Browsing

Add a Date Picker element (Add panel → search 'Date Picker') above your Calendar for users to jump to a specific date or filter by date range. In the Date Picker's Settings, set Mode to 'range' for date-range filtering. Create two page variables: 'filterStart' (Text) and 'filterEnd' (Text). Bind the Date Picker's On date select workflow to set both variables from the emitted date range object. Update your 'bookings' collection filter to add: start_time → is after → filterStart → Ignore if empty, and start_time → is before → filterEnd → Ignore if empty. Add a Fetch collection action in the On date select workflow to reload filtered data. The Calendar will automatically re-render with the filtered events.

Expected result: Selecting a date range in the Date Picker filters the calendar to show only available slots within that period.

Complete working example

booking-confirmation-edge-function.ts
1// Injection point: Supabase Dashboard → Edge Functions → New Function
2// Function name: confirm-booking
3// Called from WeWeb booking-success page's On page load workflow
4// via Supabase workflow action: Invoke Edge Function
5
6import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
7import Stripe from 'https://esm.sh/stripe@14';
8
9const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!, {
10 apiVersion: '2023-10-16',
11 httpClient: Stripe.createFetchHttpClient()
12});
13
14const supabase = createClient(
15 Deno.env.get('SUPABASE_URL')!,
16 Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
17);
18
19Deno.serve(async (req) => {
20 const { session_id, booking_id } = await req.json();
21
22 if (!session_id || !booking_id) {
23 return new Response(JSON.stringify({ error: 'Missing parameters' }), {
24 status: 400, headers: { 'Content-Type': 'application/json' }
25 });
26 }
27
28 // Verify payment with Stripe
29 const session = await stripe.checkout.sessions.retrieve(session_id);
30
31 if (session.payment_status !== 'paid') {
32 return new Response(JSON.stringify({ error: 'Payment not completed' }), {
33 status: 402, headers: { 'Content-Type': 'application/json' }
34 });
35 }
36
37 // Update booking status in Supabase
38 const { error } = await supabase
39 .from('bookings')
40 .update({
41 status: 'booked',
42 stripe_payment_id: session.payment_intent as string
43 })
44 .eq('id', booking_id)
45 .eq('status', 'available'); // Idempotency check
46
47 if (error) {
48 return new Response(JSON.stringify({ error: error.message }), {
49 status: 500, headers: { 'Content-Type': 'application/json' }
50 });
51 }
52
53 return new Response(JSON.stringify({ success: true }), {
54 status: 200, headers: { 'Content-Type': 'application/json' }
55 });
56});

Common mistakes

Why it's a problem: Updating booking status to 'booked' directly from the WeWeb frontend without checking for race conditions

How to avoid: Use Supabase's UPDATE with a WHERE status = 'available' clause. If two users try to book the same slot simultaneously, only the first UPDATE will match the condition and succeed. Check the number of updated rows in the response — if 0, show an error 'This slot was just taken' and refetch the collection.

Why it's a problem: Binding Calendar events to raw Supabase data without mapping to the expected event schema

How to avoid: The Calendar component requires objects with 'start', 'end', and 'title' keys. Supabase data uses 'start_time' and 'end_time'. Always map your collection data using a formula before binding to the Calendar's Events property.

Why it's a problem: Not handling timezone differences between the server and the user's browser

How to avoid: Store all times in Supabase as TIMESTAMPTZ (with timezone, UTC). The Date Picker component supports timezone configuration — set its 'timezone' property to the user's local timezone using JavaScript's Intl.DateTimeFormat().resolvedOptions().timeZone. Display times using WeWeb's date formatting formulas which respect local timezone.

Best practices

  • Store all booking times as TIMESTAMPTZ in UTC in Supabase and convert to local timezone only in the display layer
  • Use optimistic locking (WHERE status = 'available') on Supabase UPDATEs to handle concurrent booking attempts gracefully
  • Always verify Stripe payments server-side (Edge Function) before marking a booking as confirmed — never trust the frontend
  • Send booking confirmation emails via a Supabase Database Function trigger rather than from WeWeb, so emails are sent even if the user closes the browser after payment
  • Pre-generate available time slots server-side (e.g., via a scheduled Supabase function) rather than computing them dynamically in WeWeb formulas
  • Use CSS classes (not inline styles) to color-code event types so styles can be updated globally from App Settings → Custom Code
  • Add a 'Refresh calendar' button with a Fetch collection action as a fallback for users who lose their Realtime connection

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I'm building a booking system with WeWeb frontend and Supabase backend. My bookings table has: id, start_time (timestamptz), end_time (timestamptz), status (available/booked/cancelled), customer_id, provider_id. Write a PostgreSQL function that generates available 30-minute slots for a given provider between two dates, excluding already-booked slots.

WeWeb Prompt

In my WeWeb booking app, I have a Calendar element and a 'bookings' Supabase collection. Help me write the formula for the Calendar's Events property that maps the collection data to the correct Calendar event format, with green color for 'available' slots and blue for 'booked' slots owned by the current user.

Frequently asked questions

Does the WeWeb Calendar component support recurring events?

The Calendar component (Vue Cal) does not natively generate recurring events from a recurrence rule (RRULE). To display recurring events, you need to pre-expand them into individual records in your Supabase database. For example, a weekly recurring booking would be stored as 52 individual rows. You can automate this with a Supabase scheduled Edge Function that generates recurring slots in advance.

Can users drag and drop events to reschedule them on the Calendar?

Yes. The Calendar component supports drag-and-drop rescheduling. Enable it in the Calendar's Settings panel under the Drag & Drop section. When a user drags an event to a new time, the Calendar emits an 'On event drag' event — add a workflow to this trigger that calls a Supabase Database Update action to persist the new start_time and end_time.

How do I block out dates (e.g., holidays, provider unavailability) on the Calendar?

Insert 'blocked' records into your bookings table for the unavailable time ranges (status: 'blocked'). In your Calendar events formula, include these blocked records with a distinct CSS class (e.g., 'slot-blocked') that applies a grey/strikethrough style. To prevent users from clicking blocked slots, add an If/Else action in the On event click workflow that checks if the clicked event's status is 'blocked' and skips the booking popup if true.

What is the difference between the WeWeb Calendar and the Calendly plugin?

The WeWeb Calendar component is a fully custom, data-driven calendar you control — all event data lives in your own Supabase database. The Calendly plugin embeds a Calendly scheduling widget using your Calendly account. Use the native Calendar when you need a custom booking database, custom UI, and Stripe payments. Use the Calendly plugin when you want zero backend setup and are happy with Calendly's standard functionality and email system.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.