Skip to main content
RapidDev - Software Development Agency
Lovable PromptsProductivityIntermediate

Build a Scheduling App in Lovable

A Calendly-style scheduling app where hosts define availability windows and event types, share a public booking link, and guests book slots — with DB-level conflict prevention via btree_gist exclusion constraint and optional Google Calendar sync.

Time to MVP

1 day

Credits

~100–150 credits for full chain

Difficulty

Intermediate

Cloud features

3

TL;DR

The one-paragraph version before you dive in.

Paste the starter prompt into Lovable Build mode and get a working Calendly-style scheduler: each user defines availability windows and event types, shares a public /username/30min link, and the system prevents double-booking at the database layer with a btree_gist exclusion constraint. Full chain is ~100–150 credits on Pro $25/mo. Honest warning: just buy Calendly unless you need scheduling embedded inside your own product.

Setup checklist

Complete these steps in Lovable before pasting the starter prompt — takes about 5 minutes.

Cloud tab settings

Database

Stores profiles, event_types, availability_rules, and meetings. The starter prompt creates all four tables plus the btree_gist exclusion constraint that prevents overlapping meetings.

  1. 1Click the + button next to Preview to open the Cloud panel
  2. 2Click Database — leave the Table Editor empty for now
  3. 3The starter prompt generates supabase/migrations/0001_scheduling_schema.sql — let it run before any manual table changes

Auth

Hosts sign in with email/password. Guests book without an account. Google OAuth is added in a follow-up prompt for Calendar sync.

  1. 1Cloud tab → Users & Auth
  2. 2Confirm Email sign-in is enabled (default)
  3. 3Leave 'Allow new users to sign up' ON — hosts self-register

Edge Functions

get-slots computes free time increments from availability rules. create-meeting does the conflict-safe insert. Both run server-side to prevent race conditions.

  1. 1Cloud tab → Edge Functions — leave empty for now
  2. 2Functions appear automatically after the starter prompt pushes supabase/functions/ to the repo
  3. 3After the starter prompt runs, confirm get-slots and create-meeting appear in this tab

Secrets (Cloud tab → Secrets)

RESEND_API_KEY

Purpose: Sends booking confirmation emails and 1h-before meeting reminders from the create-meeting edge function

Where to get it: Go to https://resend.com/api-keys, sign in, click Create API Key, set Sending access, copy the key starting with re_

SITE_URL

Purpose: Used in email templates to build the correct booking confirmation and reschedule links

Where to get it: After publishing (top-right Publish button), copy the production URL or custom domain (e.g. https://scheduler.yourdomain.com)

GOOGLE_CLIENT_ID

Purpose: Required only for the Google Calendar sync follow-up prompt. Skip during the initial build.

Where to get it: Google Cloud Console (console.cloud.google.com) → APIs & Services → Credentials → Create OAuth 2.0 Client ID → Web application — copy the Client ID

GOOGLE_CLIENT_SECRET

Purpose: Required only for the Google Calendar sync follow-up prompt. Skip during the initial build.

Where to get it: Same Google Cloud Console credential page — copy the Client Secret alongside the Client ID

Preflight checklist

  • You're in a fresh Lovable project (Vite + React + TypeScript + Tailwind + shadcn/ui already scaffolded)
  • You're on Pro $25/mo — the exclusion constraint setup and Google OAuth iteration will use multiple credits cycles beyond the Free tier
  • Google Calendar sync only works from a PUBLISHED domain (not the Lovable preview iframe) — plan to test on your deployed URL, not the preview
  • The btree_gist extension must be enabled in the migration before the exclusion constraint is created — the starter prompt includes this

The starter prompt — paste this first

Copy this. Paste it into Lovable Agent Mode (the default chat at the bottom-left of the editor). Hit send.

lovable-agent-mode.txt
~40–60 credits
Build a Calendly-style scheduling app. Stack: React + Vite + TypeScript + Tailwind + shadcn/ui (already scaffolded), React Router v6, Supabase JS client against Lovable Cloud.

## Database schema (migration: supabase/migrations/0001_scheduling_schema.sql)

First enable the btree_gist extension: CREATE EXTENSION IF NOT EXISTS btree_gist;

Create four tables:

1. `profiles` — id (uuid references auth.users.id on delete cascade, primary key), slug (text not null unique), full_name (text not null), timezone (text not null default 'UTC'), google_refresh_token (text), created_at (timestamptz default now()).
   RLS: enable. Public SELECT on slug + full_name only: FOR SELECT USING (true) — but create a view or column-level: use a VIEW `public_profiles` that selects only id, slug, full_name for anon; the actual profiles table SELECT is per-user: FOR SELECT TO authenticated USING (id = auth.uid()).
   Simpler: FOR SELECT TO anon, authenticated USING (true) but only expose slug + full_name columns via the API by using column-level security (REVOKE SELECT ON profiles FROM anon; GRANT SELECT (id, slug, full_name) ON profiles TO anon;).

2. `event_types` — id (uuid pk default gen_random_uuid()), owner_id (uuid references auth.users.id not null), slug (text not null), name (text not null), duration_min (int not null check (duration_min in (15,30,45,60,90))), buffer_min (int default 0), description (text), active (bool default true), created_at (timestamptz default now()), UNIQUE (owner_id, slug).
   RLS: enable. FOR SELECT TO anon, authenticated USING (active = true). For owner write: FOR INSERT TO authenticated WITH CHECK (owner_id = auth.uid()); FOR UPDATE TO authenticated USING (owner_id = auth.uid()); FOR DELETE TO authenticated USING (owner_id = auth.uid()).

3. `availability_rules` — id (uuid pk default gen_random_uuid()), owner_id (uuid references auth.users.id not null), weekday (int not null check (weekday between 0 and 6)), start_time (time not null), end_time (time not null), check (end_time > start_time), created_at (timestamptz default now()).
   RLS: enable. FOR SELECT TO authenticated USING (owner_id = auth.uid()). FOR INSERT/UPDATE/DELETE TO authenticated WITH CHECK/USING (owner_id = auth.uid()). Anon cannot read availability_rules directly — they get free slots via the get-slots edge function.

4. `meetings` — id (uuid pk default gen_random_uuid()), owner_id (uuid references auth.users.id not null), event_type_id (uuid references event_types.id not null), guest_email (text not null), guest_name (text not null), starts_at (timestamptz not null), ends_at (timestamptz not null), status (text not null default 'confirmed' check (status in ('confirmed','cancelled'))), google_event_id (text), created_at (timestamptz default now()).
   Exclusion constraint: CONSTRAINT meetings_no_overlap EXCLUDE USING gist (owner_id WITH =, tstzrange(starts_at, ends_at, '[)') WITH &&).
   RLS: enable. FOR SELECT TO authenticated USING (owner_id = auth.uid()). FOR INSERT TO authenticated — blocked (use edge function only; no client INSERT). FOR UPDATE TO authenticated USING (owner_id = auth.uid()).

## Layouts

`src/layouts/AppLayout.tsx` — authenticated host layout. Sidebar (240px): logo, nav links (Dashboard / Event Types / Availability / Integrations / View My Page). Bottom: user avatar + name + sign out. Main area scrollable. Wrap with `<HostGuard>` that checks supabase.auth.getSession(); if not signed in, redirect to /signin.

`src/layouts/PublicLayout.tsx` — minimal layout for /[user-slug] and /[user-slug]/[event-slug] booking pages. Just a centered container, no sidebar.

## Pages (add to src/App.tsx)

- `/dashboard` — Dashboard.tsx: table of upcoming confirmed meetings. Columns: guest name, event type, starts_at (formatted in owner's timezone), status. Actions: Cancel meeting button.
- `/event-types` — EventTypes.tsx: card grid of event types (name, duration, active toggle). Add Event Type button opens EventTypeDialog (form: name, slug, duration_min Select, buffer_min, description, active toggle).
- `/event-types/new` — redirect to /event-types with dialog open.
- `/availability` — Availability.tsx: AvailabilityGrid component. 7-column grid (Mon–Sun). Each column has a toggle (Available / Unavailable) and a time-range input (start_time, end_time). Save button writes to availability_rules table.
- `/integrations` — Integrations.tsx: placeholder card for 'Google Calendar' with 'Connect' button (wired in follow-up).
- `/[user-slug]` — booking/HostProfile.tsx: shows host's name, list of active event types as cards with duration and description. Click an event type → /[user-slug]/[event-slug].
- `/[user-slug]/[event-slug]` — booking/SlotPicker.tsx: date picker (shadcn Calendar), then on date select, fetch free slots from get-slots edge function (GET /functions/v1/get-slots?owner_slug=X&event_type_slug=Y&date=YYYY-MM-DD). Show slots as buttons. On slot click, show confirm form: guest_name and guest_email inputs. Submit calls create-meeting edge function.

## Key components

`src/components/SlotPicker.tsx` — date + time slot picker. On date change, GET /functions/v1/get-slots with the owner's slug, event type slug, and selected date. Show each available slot as a button. Disabled state when loading.

`src/components/AvailabilityGrid.tsx` — 7-column weekday grid with time range inputs per day. Read from availability_rules on mount, write on Save.

`src/components/HostGuard.tsx` — checks session, redirects to /signin if not authenticated.

## Edge functions

`supabase/functions/get-slots/index.ts` — GET with query params owner_slug, event_type_slug, date (YYYY-MM-DD). Steps:
1. Look up the host's id from profiles.slug.
2. Fetch the event_type for that owner_id + slug to get duration_min and buffer_min.
3. Fetch availability_rules for that owner and the weekday matching the requested date.
4. Generate all possible slot start times within the availability window at duration_min intervals.
5. Fetch existing confirmed meetings on that date where status='confirmed'.
6. Remove any slots that overlap with existing meetings (slot_start < meeting.ends_at AND slot_end > meeting.starts_at).
7. Return { slots: ['HH:MM', ...], host_timezone: 'America/New_York', date: 'YYYY-MM-DD' }.

`supabase/functions/create-meeting/index.ts` — POST JSON { owner_slug, event_type_slug, guest_email, guest_name, starts_at (ISO 8601) }. Steps:
1. CORS preflight: if req.method === 'OPTIONS' return new Response('ok', { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization,content-type' } }).
2. Use service-role Supabase client.
3. Look up owner's id from profiles.slug.
4. Look up event_type for owner_id + event_type_slug to get duration_min.
5. Compute ends_at = starts_at + duration_min minutes.
6. INSERT INTO meetings (owner_id, event_type_id, guest_email, guest_name, starts_at, ends_at, status) VALUES (...) — the btree_gist exclusion constraint will raise error 23P01 on overlap.
7. On success, send confirmation email via Resend to guest_email (RESEND_API_KEY from Deno.env.get).
8. Return 200 { ok: true, meeting_id }.
9. On exclusion constraint error (Postgres error code 23P01), return 409 { ok: false, reason: 'slot_taken' }.

## Styling

Clean indigo + slate, lots of whitespace. Public booking page: single-column, mobile-first, max-width 480px centered. Host dashboard: desktop-first, sidebar+main. Use shadcn Calendar, Select, Card, Sheet, Dialog, Skeleton, Toast.

Install date-fns and date-fns-tz as dependencies.

Generate migration first, then layouts, then pages in order listed, then edge functions.

What this prompt generates

  • Creates a SQL migration with 4 tables, RLS policies, btree_gist exclusion constraint on meetings to prevent double-booking at the DB layer
  • Scaffolds AppLayout (host dashboard with sidebar) and PublicLayout (clean booking page) with HostGuard auth gate
  • Generates 7 pages: dashboard, event types CRUD, availability grid, integrations placeholder, host profile, slot picker with confirmation form
  • Creates get-slots edge function that computes free time slots from availability rules minus existing meetings
  • Creates create-meeting edge function with CORS, conflict-safe insert, and Resend confirmation email

Paste into: Lovable Agent Mode (the default chat at the bottom-left of the editor)

Expected output

What Lovable will generate after the starter prompt runs successfully.

Files
supabase/migrations/0001_scheduling_schema.sql

4 tables + RLS + btree_gist extension + exclusion constraint on meetings

src/layouts/AppLayout.tsx

Host sidebar layout with HostGuard

src/layouts/PublicLayout.tsx

Minimal centered layout for public booking pages

src/components/HostGuard.tsx

Auth gate — redirects unauthenticated users to /signin

src/pages/Dashboard.tsx

Upcoming meetings table with cancel action

src/pages/EventTypes.tsx

Event type card grid with create/edit dialog

src/pages/Availability.tsx

7-column weekday availability grid editor

src/pages/Integrations.tsx

Placeholder for Google Calendar connect

src/pages/booking/HostProfile.tsx

Public host profile with event type list

src/pages/booking/SlotPicker.tsx

Date picker + slot buttons + guest confirmation form

src/components/SlotPicker.tsx

Reusable slot button grid

src/components/AvailabilityGrid.tsx

Weekday x time-of-day availability editor

supabase/functions/get-slots/index.ts

Computes free slot list from availability rules minus existing meetings

supabase/functions/create-meeting/index.ts

Conflict-safe meeting insert with exclusion constraint handling

Routes
/dashboard

Host's upcoming meetings list with cancel action

/event-types

Event type CRUD with duration and buffer settings

/availability

Weekly availability window editor

/integrations

Google Calendar connection (wired in follow-up)

/:userSlug

Public host profile with event type cards

/:userSlug/:eventSlug

Public slot picker and booking confirmation

/signin

Host email+password sign-in

/signup

Host registration

DB Tables
profiles
ColumnType
iduuid references auth.users primary key
slugtext not null unique
full_nametext not null
timezonetext not null default 'UTC'
google_refresh_tokentext nullable

RLS: Public SELECT on slug + full_name only. Per-user full access.

event_types
ColumnType
iduuid primary key
owner_iduuid references auth.users
slugtext not null
nametext not null
duration_minint check in (15,30,45,60,90)
buffer_minint default 0
activebool default true

RLS: Public SELECT where active. Per-owner write.

availability_rules
ColumnType
iduuid primary key
owner_iduuid references auth.users
weekdayint check between 0 and 6
start_timetime not null
end_timetime not null

RLS: Per-owner read+write only. Anon access via get-slots edge function.

meetings
ColumnType
iduuid primary key
owner_iduuid references auth.users
event_type_iduuid references event_types
guest_emailtext not null
guest_nametext not null
starts_attimestamptz not null
ends_attimestamptz not null
statustext check in ('confirmed','cancelled')
google_event_idtext nullable

RLS: Per-owner SELECT. INSERT via edge function only. Exclusion constraint prevents overlapping meetings per owner.

Components
SlotPicker

Date picker + available slot buttons wired to get-slots edge function

AvailabilityGrid

7-column weekday editor for availability rules

HostGuard

Redirects unauthenticated visitors to /signin

EventTypeCard

Card showing event type name, duration, and active status

Follow-up prompts

Paste these into Agent Mode one by one, in order, after the starter prompt finishes.

1

Add DB-level overlap exclusion constraint (non-negotiable safety net)

DB-level exclusion constraint that makes double-booking impossible regardless of race conditions or client bugs

~20 credits
prompt
Add the btree_gist exclusion constraint to the meetings table if the migration didn't include it:

Run this in Cloud → Database → SQL Editor:

CREATE EXTENSION IF NOT EXISTS btree_gist;

ALTER TABLE meetings ADD CONSTRAINT meetings_no_overlap EXCLUDE USING gist (owner_id WITH =, tstzrange(starts_at, ends_at, '[)') WITH &&) WHERE (status = 'confirmed');

This constraint makes it literally impossible for two confirmed meetings per host to overlap at the database layer — no client bug, no race condition, and no get-slots computation error can create a double-booking. The exclusion only applies to status='confirmed' so cancelled meetings don't block the slot.

Also update the create-meeting edge function to handle Postgres error code 23P01 (exclusion constraint violation) explicitly: catch the error, check if error.code === '23P01', return 409 { ok: false, reason: 'slot_taken' }. In SlotPicker.tsx, on a 409 response, re-fetch slots and show a toast: 'That slot was just taken. Please pick another time.'

When to use: Paste this immediately after the starter prompt — this is the single most important hardening step

2

Add Google Calendar 2-way sync via OAuth

Google Calendar OAuth connection and automatic calendar event creation when a meeting is booked

~50 credits
prompt
Add Google Calendar sync for hosts. This only works after you have deployed to a production URL and added that URL to the Google OAuth client's Authorized redirect URIs.

Prerequisites: Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in Cloud → Secrets. In Google Cloud Console → APIs & Services → Credentials → your OAuth 2.0 Client ID → add Authorized redirect URIs: https://YOUR-PROJECT-REF.supabase.co/auth/v1/callback and your published Lovable domain + /auth/callback.

Steps:

1. In Integrations.tsx, wire the 'Connect Google Calendar' button to call supabase.auth.signInWithOAuth({ provider: 'google', options: { scopes: 'https://www.googleapis.com/auth/calendar.events', redirectTo: window.location.origin + '/integrations' } }). After redirect back, Supabase issues a session with the Google access token.

2. In Cloud tab → Users & Auth → OAuth → enable Google provider, paste GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET.

3. After OAuth, call an edge function store-google-token: it reads the Google refresh_token from the Supabase session metadata and writes it to profiles.google_refresh_token for the authenticated user.

4. Update create-meeting edge function: after inserting the meeting, if profiles.google_refresh_token is set, call the Google Calendar API: POST https://www.googleapis.com/calendar/v3/calendars/primary/events with Authorization: Bearer {access_token} (refresh the access token using the stored refresh_token if needed). Save the returned event id to meetings.google_event_id.

5. In Dashboard.tsx, show a Google Calendar icon on meeting rows where google_event_id is set.

When to use: Only after deploying to production — Google OAuth callback does not work from the Lovable preview iframe

3

Add email confirmations and 1h-before reminders via Resend

Booking confirmation email to guest and 1h-before reminder, scheduled by pg_cron

~30 credits
prompt
Add two Resend email flows:

1. In create-meeting edge function, after successful insert, send a confirmation email to guest_email with:
   - Subject: 'Your meeting with [host.full_name] is confirmed'
   - Body: meeting date/time formatted in the host's timezone (use formatInTimeZone from date-fns-tz in Deno), event type name, duration, and a cancel link pointing to /cancel/[meeting_id].
   Use RESEND_API_KEY from Deno.env.get('RESEND_API_KEY') with from address noreply@your-domain.com.

2. Create supabase/functions/send-reminders/index.ts: queries all meetings where starts_at is between now() + interval '58 minutes' and now() + interval '62 minutes' AND status='confirmed'. For each, sends a reminder email to guest_email via Resend: 'Your meeting with [host] starts in 1 hour'.

3. Schedule the reminder function to run every 30 minutes using pg_cron: CREATE EXTENSION IF NOT EXISTS pg_cron; SELECT cron.schedule('send-meeting-reminders', '*/30 * * * *', $$SELECT net.http_post(url:='https://[project-ref].supabase.co/functions/v1/send-reminders', headers:='{"Authorization": "Bearer [service-role-key]"}'::jsonb)$$);

Make sure RESEND_API_KEY and SITE_URL are set in Cloud → Secrets before running.

When to use: Once RESEND_API_KEY is in Secrets and your Resend domain is verified with SPF + DKIM records

4

Add cancellation and reschedule flow

Cancellation via email link, reschedule flow with atomic old-cancel + new-book transaction

~40 credits
prompt
Add cancellation from the confirmation email and a reschedule option:

1. Create a page /cancel/[meeting-id] — Cancel.tsx. It fetches the meeting by id (using a public-readable meetings view or a verify-meeting edge function that checks the meeting exists). Shows event name, date, guest name. Cancel button calls an edge function cancel-meeting: marks meetings.status='cancelled', if google_event_id is set calls Google Calendar API DELETE, sends a 'Meeting cancelled' email via Resend.

2. Add a 'Reschedule' link in the confirmation email pointing to /reschedule/[meeting-id].

3. Create /reschedule/[meeting-id] — Reschedule.tsx. Shows the same SlotPicker component. On new slot selection and confirm, call a reschedule-meeting edge function that: (a) sets old meeting status='cancelled', (b) inserts a new meeting row (which goes through the exclusion constraint), (c) if google_event_id is set, DELETE old Google Calendar event and CREATE new one, (d) sends a 'Rescheduled' confirmation email. Wrap steps a-b in a transaction so an API error rolls back the meeting update.

4. In Dashboard.tsx, add a Cancel button for each meeting row that calls the cancel-meeting edge function.

When to use: High-value feature — implement after the base booking flow is working; no-shows drop significantly with easy reschedule

5

Add custom branding on the public booking page

Host-configurable brand color, avatar, and tagline on the public booking page

~30 credits
prompt
Let hosts customize their public booking page appearance:

1. Add a branding jsonb column to profiles: branding (jsonb default '{}'). The jsonb accepts { primary_color: '#4F46E5', tagline: 'string', avatar_path: 'string' }.

2. Add a Branding section to the Integrations page (or create a new /settings page): color picker (input type='color' for primary_color), tagline text input, avatar upload (Store avatar in Lovable Cloud Storage bucket 'avatars', store path in branding.avatar_path).

3. In HostProfile.tsx and SlotPicker.tsx, read the host's branding from the profiles query and apply: set CSS custom property --brand-primary to branding.primary_color on the page root div, show avatar if set, show tagline below host name.

4. Create the avatars bucket: Cloud tab → Storage → Create bucket 'avatars', set Public ON.

Do not change any booking logic — this is purely cosmetic.

When to use: Purely cosmetic, ship last — focus on core booking reliability first

Common errors

Real error strings you'll see. Find yours, paste the fix prompt.

duplicate key value violates exclusion constraint "meetings_no_overlap"

This is the exclusion constraint working exactly as designed — two guests tried to book the same slot simultaneously and the second one was rejected at the database layer.

Fix — paste into Lovable Agent Mode
This error should NOT be shown to the user as-is. In create-meeting edge function, catch the Postgres error and check if (error.code === '23P01'). If yes, return HTTP 409 with JSON { ok: false, reason: 'slot_taken' }. In SlotPicker.tsx, on a 409 response, re-fetch available slots from get-slots (the taken slot will now be missing) and show a shadcn Toast: 'That slot was just taken by another booking. Please pick another time.'
redirect_uri_mismatch — Error 400 from Google OAuth

Google's OAuth client only allows callbacks to redirect URIs explicitly added in Google Cloud Console. The Lovable preview URL or your custom domain was not added.

Manual fix

Open Google Cloud Console → APIs & Services → Credentials → click your OAuth 2.0 Client ID → Authorized redirect URIs → Add URI: https://[your-supabase-project-ref].supabase.co/auth/v1/callback AND your published Lovable domain + /auth/callback. Also add the Lovable preview URL if you want to test there (but preview OAuth never fully works — use your deployed URL).

Cannot insert null into column 'timezone' of relation 'profiles'

Lovable's first profile-creation flow didn't capture the host's timezone. The get-slots edge function requires a timezone to compute slots correctly.

Fix — paste into Lovable Agent Mode
In the sign-up flow (after supabase.auth.signUp succeeds), immediately call: const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; await supabase.from('profiles').upsert({ id: user.id, slug: generateSlug(email), full_name: email.split('@')[0], timezone: tz }); before redirecting to /dashboard. Also add a timezone Select to the profile settings page so hosts can correct the auto-detected value.
slots show in UTC instead of guest's local timezone (SlotPicker shows 15:00 instead of 8:00 AM)

get-slots returns UTC timestamps but SlotPicker formats them with .toISOString().slice(11,16) which always shows UTC regardless of the user's timezone.

Fix — paste into Lovable Agent Mode
In SlotPicker.tsx, detect the guest's timezone with const guestTz = Intl.DateTimeFormat().resolvedOptions().timeZone. For each slot returned from get-slots, format it with formatInTimeZone(new Date(slot), guestTz, 'h:mm a') from date-fns-tz. Add a line below the date showing 'Times shown in your timezone (PST)' using the timezone abbreviation. Pass guestTz as a query param to get-slots so the edge function can return slots pre-formatted if preferred.
Google Calendar event created twice on reschedule

The reschedule flow created a new Calendar event but didn't delete the old one first — meetings.google_event_id from the old meeting was never used to call Calendar API DELETE.

Fix — paste into Lovable Agent Mode
In the reschedule-meeting edge function, before creating the new meeting and new Calendar event: (1) fetch the old meeting's google_event_id; (2) if it is not null, call DELETE https://www.googleapis.com/calendar/v3/calendars/primary/events/{google_event_id} with the host's refreshed access token; (3) only then insert the new meeting row and create the new Calendar event. Wrap all three steps in a try/catch so a Google API failure doesn't leave a dangling meeting row.

Cost reality

What this build actually costs — no surprises on your card.

Recommended Lovable plan

Pro $25/mo — Google OAuth setup and the exclusion constraint iteration will each need several back-and-forth cycles; Free's 30-credit monthly cap runs out mid-debug

Monthly run cost breakdown

~100–150 credits — starter ~40, exclusion constraint ~20, Google Calendar sync ~50, email reminders ~30, plus 2 retry rounds total
ItemCost
Lovable Pro

During active build only — cancel after you stop iterating

$25/mo
Supabase (Lovable Cloud)

A personal scheduler DB stays well under 500MB free tier indefinitely

$0
Resend

Free 3K emails/mo covers ~1K meetings/mo (1 confirmation + 1 reminder each)

$0
Google Calendar API

Calendar API is free with a Google Cloud project — no billing unless you exceed 1M requests/day

$0
Custom domain

Via Lovable Publish → Custom Domain on any paid plan

~$12/yr

Scaling notes: Nothing breaks cost-wise until ~1K meetings/mo when Resend free tier runs out. Supabase Free is fine for the lifetime of a personal scheduler. The bigger scaling concern is business value: at ~100 meetings/mo you are getting more value from Calendly's $10/mo than from maintaining this yourself.

Production checklist

Steps to take before you share the URL with real users.

Domain & SSL

  • Connect custom domain

    Top-right Publish → Custom domain → add your domain → update DNS CNAME → verify. Then update SITE_URL in Cloud → Secrets to your new domain.

  • Update Google OAuth redirect URIs

    After domain switch, add the new domain + /auth/callback to Authorized redirect URIs in Google Cloud Console → APIs & Services → Credentials. Booking pages with Google sync will fail until this is done.

Email

  • Verify Resend domain

    Resend dashboard → Domains → Add Domain → copy SPF + DKIM records → add to DNS provider → wait for green status. Without this, confirmation emails land in spam.

  • Test confirmation email end-to-end

    Book a test meeting on your deployed URL with a real email address. Check inbox and spam folder. Verify the cancel link in the email works and points to your domain, not undefined.

Double-booking audit

  • Verify exclusion constraint is active

    In Cloud → Database → SQL Editor, run: SELECT conname, contype FROM pg_constraint WHERE conname = 'meetings_no_overlap'; — should return one row. If missing, run the exclusion constraint follow-up prompt.

  • Test concurrent booking

    Open two incognito windows, navigate to the same /username/30min page, pick the same slot in both, submit both simultaneously. Only one should succeed; the other should show 'That slot was just taken'.

Google Calendar (if enabled)

  • Handle expired refresh tokens

    Add a check in Dashboard.tsx: on app load, call a test-calendar-auth edge function that tries a lightweight Google Calendar API call using the stored refresh_token. If it returns 401, show a banner: 'Reconnect Google Calendar' linking to /integrations. Refresh tokens expire after 6 months of disuse.

Frequently asked questions

Why can't I just check for conflicts in React before inserting a meeting?

Client-side conflict checks fail under concurrent load. If two guests open the same slot picker, both see the slot as available, both confirm at the same millisecond, and both checks pass before either insert lands. The only reliable solution is a database-layer constraint. The starter prompt adds a btree_gist exclusion constraint on the meetings table that makes overlapping meetings per host physically impossible to insert — the database rejects the second one with error code 23P01, which the edge function catches and returns as a 409 slot_taken response.

Does the Google Calendar sync work in Lovable preview?

No. Google's OAuth callback requires a redirect URI that exactly matches what you registered in Google Cloud Console. The Lovable preview URL (ending in .lovable.app or similar) is not a stable URI you can register and trust for production. You must deploy your project using the Publish button, copy the production URL, add it to your Google OAuth client's Authorized redirect URIs, and test the full OAuth flow on the deployed site. The Integrations placeholder in the starter gives you a place to wire this up — it intentionally does not wire OAuth in the preview.

Can multiple team members share a single booking link?

The starter prompt builds a personal scheduler — one host, one set of availability rules, one booking link per event type. For team scheduling (round-robin assignment, pick-your-host), you need the round-robin team scheduling follow-up prompt, which adds a host_pool array to event_types and a create-meeting edge function that picks the team member with the fewest meetings this week. This adds roughly 60 credits to the chain and requires careful multi-user RLS testing.

How do I handle Daylight Saving Time when storing availability windows?

The availability_rules table stores weekday + start_time + end_time as plain SQL time values without timezone context. This means a rule saying '9am Monday' is stored as 09:00 regardless of DST. The get-slots edge function must apply the host's IANA timezone (from profiles.timezone) when computing slot UTC times — if the host is in America/New_York and DST shifts an hour, slots near the boundary can appear or disappear by one hour. This is a documented known edge case. The recommended mitigation is to display a 'Check your availability after every DST change' reminder in the Availability page and let the host manually re-verify slots.

What happens if a guest's timezone is different from mine?

get-slots returns timestamps in UTC. SlotPicker.tsx detects the guest's timezone from Intl.DateTimeFormat().resolvedOptions().timeZone and formats each slot using formatInTimeZone from date-fns-tz. The booking confirmation email should state the meeting time in BOTH the host's timezone and the guest's timezone — this prevents the classic 'I thought it was 2pm your time' miscommunication. The email follow-up prompt includes this dual-timezone confirmation format.

Can I take payment for paid sessions through this scheduler?

Not in the starter prompt. To monetize paid sessions, you would add a Stripe Checkout step between slot selection and booking confirmation: the guest pays first, the payment_intent.succeeded webhook fires, and only then does the create-meeting edge function insert the meeting row. This is a significant addition — closer to the booking platform prompt kit than the scheduling app. If you just want to take a deposit to reduce no-shows, the booking platform prompt is a better starting point.

How do I let guests reschedule from the confirmation email?

The cancellation and reschedule follow-up prompt adds a /reschedule/[meeting-id] page and a matching edge function. The confirmation email includes a reschedule link. The reschedule flow atomically cancels the old meeting (setting status='cancelled') and creates the new one in the same transaction, so the exclusion constraint prevents the new slot from being taken mid-process. If Google Calendar sync is enabled, the old Calendar event is deleted before the new one is created.

Should I really build this instead of just using Calendly?

Honestly, just use Calendly for 95% of use cases. Calendly is $10/mo, never breaks, handles edge cases you haven't thought of yet, and the UX is battle-tested by millions of users. Build in Lovable only if: (1) you need scheduling embedded as a feature inside your own product — not a separate /calendly.com/you link; (2) you have data residency or privacy requirements; or (3) you want to own the full meeting history and integrate it with other data in your app. If your build outgrows this prompt kit and you need custom architecture, RapidDev builds production-grade Lovable apps at $13K-$25K — book a free 30-minute consultation at rapidevelopers.com.

Need a production-grade version?

RapidDev builds production-grade Lovable apps at $13K–$25K. Book a free 30-minute consultation.

Book a free consultation

30-min call. No commitment.

Want this built for you?

We ship production apps at a fixed price — $13K–$25K, 6–10 weeks, source code yours. You've seen what it takes; we do it every week.

Get a fixed-price quote

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.