Skip to main content
RapidDev - Software Development Agency
Lovable PromptsProductivityIntermediate

Build an Event Calendar App in Lovable

A public event calendar with month/week views, organizer dashboard, RSVP with capacity caps, email reminders via Resend, and ICS export — backed by Lovable Cloud with public-read events and per-organizer write RLS.

Time to MVP

1 day

Credits

~100–150 credits for full chain

Difficulty

Intermediate

Cloud features

4

TL;DR

The one-paragraph version before you dive in.

Paste the starter prompt into Lovable Build mode and get a working event calendar: public browse with month view, RSVP with capacity caps, organizer dashboard, and ICS export — all on Supabase Cloud. The capacity race-condition edge function and timezone discipline are non-negotiable. Full chain: ~100–150 credits on a Pro $25/mo plan, about one full day of building.

Setup checklist

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

Cloud tab settings

Database

Stores events, rsvps, profiles, and categories. The starter prompt creates all four tables plus RLS migrations.

  1. 1Click the + button next to Preview in the top toolbar to open the Cloud panel
  2. 2Click Database — you'll see an empty Table Editor
  3. 3Leave it empty for now — the starter prompt generates the migration file

Auth

Organizers sign up with email/password. Anonymous RSVP is handled server-side in the edge function — no sign-in required for guests.

  1. 1Cloud tab → Users & Auth
  2. 2Confirm Email sign-in is enabled (it is by default)
  3. 3Leave 'Allow new users to sign up' ON — organizers self-register
  4. 4Disable any OAuth providers you don't need (Google, GitHub) to reduce surface area

Storage

Stores event cover images uploaded by organizers. One public bucket so images render on the public event cards.

  1. 1Cloud tab → Storage
  2. 2Click Create bucket, name it 'event-images', toggle Public ON
  3. 3Note the bucket name — the starter prompt references 'event-images' throughout

Edge Functions

Hosts the capacity-checked RSVP insert, ICS export, and 24h reminder fan-out. The starter prompt generates the function files; you don't deploy them manually.

  1. 1Cloud tab → Edge Functions — leave empty for now
  2. 2Functions deploy automatically when Lovable pushes supabase/functions/ to the repo
  3. 3After the starter prompt runs, verify functions appear in this tab

Secrets (Cloud tab → Secrets)

RESEND_API_KEY

Purpose: Sends RSVP confirmation emails and 24-hour event reminders via the send-reminders edge function

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

SITE_URL

Purpose: Used in email templates to build the correct event and unsubscribe links — without this, email links point to undefined

Where to get it: After publishing your Lovable project (top-right Publish button), copy the production URL (e.g. https://my-events.lovable.app) — or your custom domain once set up

Preflight checklist

  • You're in a fresh Lovable project (Vite + React + TypeScript + Tailwind + shadcn/ui already scaffolded — that's the Lovable default)
  • You're on Pro $25/mo — the starter + capacity edge function + email follow-ups will burn through Free's 30/mo cap before you finish
  • You've signed up for a free Resend account at resend.com and verified your sending domain (add SPF + DKIM DNS records) before testing email flow
  • You understand that timezone handling is the #1 gotcha: events store starts_at as timestamptz (UTC) plus a separate timezone IANA string, and the form must convert local input to UTC before insert

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 an event calendar app. Stack assumptions: React + Vite + TypeScript + Tailwind + shadcn/ui (already scaffolded), React Router v6 for routes, Supabase JS client against Lovable Cloud.

## Database schema (create a migration: supabase/migrations/0001_events_schema.sql)

Create four tables in the public schema:

1. `profiles` — id (uuid references auth.users.id on delete cascade, primary key), full_name (text), email (text), timezone (text not null default 'UTC'), created_at (timestamptz default now()).
   RLS: enable. Policy: per-user read+write where id = auth.uid().

2. `categories` — id (uuid pk default gen_random_uuid()), name (text not null unique), slug (text not null unique), created_at (timestamptz default now()).
   RLS: enable. Policy: public-read (anon + authenticated). Admin-write via (auth.jwt() ->> 'role' = 'admin') — skip for now, seed a few rows directly.

3. `events` — id (uuid pk default gen_random_uuid()), organizer_id (uuid references auth.users.id not null), title (text not null), slug (text not null unique), description (text), starts_at (timestamptz not null), ends_at (timestamptz not null), timezone (text not null default 'UTC'), location (text), capacity (int), cover_image_path (text), category_id (uuid references categories.id), status (text not null default 'draft' check (status in ('draft','published','cancelled'))), created_at (timestamptz default now()).
   Constraint: check (ends_at > starts_at).
   RLS: enable.
   Policies:
   - Public SELECT: FOR SELECT TO anon, authenticated USING (status = 'published').
   - Organizer write: FOR INSERT TO authenticated WITH CHECK (organizer_id = auth.uid()); FOR UPDATE TO authenticated USING (organizer_id = auth.uid()) WITH CHECK (organizer_id = auth.uid()); FOR DELETE TO authenticated USING (organizer_id = auth.uid()).

4. `rsvps` — id (uuid pk default gen_random_uuid()), event_id (uuid references events.id not null), user_id (uuid references auth.users.id nullable), guest_email (text nullable), guest_name (text nullable), status (text not null default 'going' check (status in ('going','maybe','no'))), responded_at (timestamptz default now()).
   Constraint: unique (event_id, user_id) where user_id is not null.
   RLS: enable.
   Policies:
   - INSERT: blocked for direct client; use edge function only (no anon/authenticated INSERT policy here — the edge function uses the service-role key).
   - SELECT for organizer: FOR SELECT TO authenticated USING (EXISTS (SELECT 1 FROM events e WHERE e.id = rsvps.event_id AND e.organizer_id = auth.uid())).
   - SELECT for own: FOR SELECT TO authenticated USING (user_id = auth.uid()).

## Layouts

Create two layout components:

`src/layouts/PublicLayout.tsx` — top nav with: Logo text 'EventSpace', nav links (Browse Events / Sign in). Clean, minimal. Use shadcn NavigationMenu.

`src/layouts/OrganizerLayout.tsx` — sidebar (240px) with nav links (My Events / New Event / Settings / Public View link), content area on the right. Wrap with `<OrganizerGuard>` that reads supabase auth session; if not authenticated, redirect to /signin.

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

- `/` — Events.tsx: grid of published events, sorted by starts_at asc. Each card shows cover image, title, date formatted in event.timezone using date-fns-tz `formatInTimeZone`, location, capacity remaining (total capacity minus rsvp count where status='going'). Include a CalendarGrid component showing the current month with dots on event days.
- `/events/[slug]` — EventDetail.tsx: full event detail. Shows starts_at formatted with `formatInTimeZone(event.starts_at, event.timezone, 'PPP p zzz')` from date-fns-tz. Show capacity: X going / Y capacity. Include RSVPDialog component (Sheet on mobile, Dialog on desktop): name + email fields for guests, or confirm with account for signed-in users. Button disabled when capacity is full. ICS download button at bottom.
- `/organizer` — organizer/Dashboard.tsx: table of organizer's events (all statuses), columns: title, starts_at, status badge, RSVP count. Actions: edit, publish/unpublish toggle, view event.
- `/organizer/events/new` — organizer/EventEditor.tsx with blank form.
- `/organizer/events/[id]/edit` — organizer/EventEditor.tsx pre-populated.
- `/signin` and `/signup` — basic email+password forms using supabase.auth.signInWithPassword and signUp.

## Key components

`src/components/EventCard.tsx` — cover image, title, formatted date (use date-fns-tz), location, category badge, RSVP count chip.

`src/components/CalendarGrid.tsx` — simple month grid (7 columns). Days with events get an indigo dot. Clicking a day scrolls to the event or navigates to it.

`src/components/RSVPDialog.tsx` — form for name + email (guest) or just confirm (signed-in). Calls POST /functions/v1/rsvp-create with JSON body { event_id, guest_name, guest_email, user_id (if signed in), status: 'going' }. Shows loading state and success confirmation.

`src/components/EventForm.tsx` — for organizer create/edit. Fields: title, description, starts_at (date + time input), ends_at, timezone (searchable Select using a list of IANA timezones), location, capacity (number), category (Select), cover image upload (Cloud Storage bucket 'event-images'), status radio (Draft / Published). On submit, combine the date+time inputs with the selected timezone using date-fns-tz `zonedTimeToUtc` before writing to Supabase.

`src/components/OrganizerGuard.tsx` — wraps protected routes.

## Edge functions

`supabase/functions/rsvp-create/index.ts` — accepts POST JSON { event_id, guest_name, guest_email, user_id, status }. 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) BEGIN transaction: SELECT id, capacity FROM events WHERE id = event_id FOR UPDATE. (4) SELECT COUNT(*) FROM rsvps WHERE event_id = $1 AND status = 'going'. (5) If count >= capacity, return 409 { error: 'capacity_full' }. (6) INSERT INTO rsvps. (7) COMMIT. Return 200 { ok: true, rsvp_id }. Include the CORS headers in every response.

`supabase/functions/ics-export/index.ts` — GET /functions/v1/ics-export?event_id=UUID. Fetches the event row, builds a VCALENDAR/VEVENT block with DTSTART/DTEND in the event.timezone, returns text/calendar with attachment header so browsers download a .ics file.

## Styling cues

Warm community palette: slate-50 background, indigo-600 primary, white cards with rounded-2xl and shadow-sm. Use shadcn Calendar, Dialog, Sheet, Select, Badge, Skeleton. Mobile-first on public pages. Organizer dashboard is desktop-first with a 2-column grid for event cards.

Install date-fns and date-fns-tz as dependencies — these are the correct packages for timezone-aware date formatting in Vite React projects.

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

What this prompt generates

  • Creates a SQL migration with 4 tables (profiles, events, rsvps, categories), per-organizer RLS policies, and a unique constraint on (event_id, user_id) for rsvps
  • Scaffolds two layouts: PublicLayout for browsing and OrganizerLayout with sidebar and OrganizerGuard
  • Generates 6 pages: event browse grid with CalendarGrid, event detail with RSVPDialog, organizer dashboard, event editor with timezone-aware form, sign-in and sign-up
  • Creates rsvp-create edge function with CORS preflight and DB-level capacity check in a transaction to prevent overselling
  • Creates ics-export edge function that returns a valid VCALENDAR .ics attachment for any published event

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_events_schema.sql

All four tables, RLS policies, and unique constraint on rsvps

src/layouts/PublicLayout.tsx

Top nav with events link and sign-in

src/layouts/OrganizerLayout.tsx

Sidebar with OrganizerGuard redirect

src/components/OrganizerGuard.tsx

Checks auth session, redirects to /signin if unauthenticated

src/pages/Events.tsx

Public event grid with CalendarGrid month view

src/pages/EventDetail.tsx

Event detail, timezone-formatted dates, RSVPDialog, ICS download

src/pages/organizer/Dashboard.tsx

Organizer's event table with status and RSVP count

src/pages/organizer/EventEditor.tsx

Create/edit form with timezone-aware date input

src/components/EventCard.tsx

Card with cover image, formatted date, capacity chip

src/components/CalendarGrid.tsx

Month grid with dots on event days

src/components/RSVPDialog.tsx

RSVP form calling rsvp-create edge function

src/components/EventForm.tsx

Organizer event create/edit form with zonedTimeToUtc

supabase/functions/rsvp-create/index.ts

Capacity-checked RSVP insert with SELECT ... FOR UPDATE transaction

supabase/functions/ics-export/index.ts

Returns .ics VCALENDAR file for any event UUID

Routes
/

Public event grid with CalendarGrid month view

/events/:slug

Event detail with RSVP dialog and ICS download

/organizer

Organizer dashboard with event table and status actions

/organizer/events/new

New event form

/organizer/events/:id/edit

Edit existing event

/signin

Email+password sign-in for organizers

/signup

Organizer sign-up

DB Tables
events
ColumnType
iduuid primary key
organizer_iduuid references auth.users
titletext not null
slugtext not null unique
starts_attimestamptz not null
ends_attimestamptz not null
timezonetext not null default 'UTC'
capacityint
statustext check in ('draft','published','cancelled')

RLS: Public SELECT where status='published'. Organizer INSERT/UPDATE/DELETE where organizer_id = auth.uid().

rsvps
ColumnType
iduuid primary key
event_iduuid references events
user_iduuid nullable references auth.users
guest_emailtext nullable
guest_nametext nullable
statustext check in ('going','maybe','no')

RLS: INSERT only via edge function (no client INSERT policy). Organizer SELECT for own events. Per-user SELECT own rsvps.

profiles
ColumnType
iduuid references auth.users primary key
full_nametext
emailtext
timezonetext not null default 'UTC'

RLS: Per-user read+write where id = auth.uid().

categories
ColumnType
iduuid primary key
nametext not null unique
slugtext not null unique

RLS: Public-read. Admin-write only.

Components
EventCard

Cover image, formatted date, location, capacity chip

CalendarGrid

Month grid with event dots and day-click navigation

RSVPDialog

RSVP form for guest or signed-in user, calls rsvp-create edge function

EventForm

Organizer create/edit form with timezone-aware date-fns-tz conversion

OrganizerGuard

Auth gate for /organizer/* routes

Follow-up prompts

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

1

Harden the RSVP edge function with CORS and idempotency

CORS headers on every response, upsert instead of duplicate-key error, structured JSON error responses

~15 credits
prompt
Update supabase/functions/rsvp-create/index.ts to be fully hardened:

1. Add CORS preflight at the very top: if (req.method === 'OPTIONS') return new Response('ok', { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization,content-type' } });

2. Include 'Access-Control-Allow-Origin': '*' in every response headers object.

3. For the upsert case: if the combination (event_id, user_id) already exists for a signed-in user, UPDATE the status instead of failing. Use INSERT INTO rsvps (...) ON CONFLICT (event_id, user_id) WHERE user_id IS NOT NULL DO UPDATE SET status = EXCLUDED.status.

4. Return structured JSON in all error cases: { ok: false, reason: 'capacity_full' } with HTTP 409, { ok: false, reason: 'event_not_found' } with HTTP 404, { ok: false, reason: 'invalid_body' } with HTTP 400.

Do not change any other logic in the function.

When to use: Paste right after the starter prompt finishes — before any browser testing, since CORS errors block the RSVP flow in the preview iframe

2

Add email confirmations and 24h reminders via Resend

RSVP confirmation emails and 24h-before event reminder emails via Resend, scheduled by pg_cron

~40 credits
prompt
Add two email flows using the Resend connector:

1. In supabase/functions/rsvp-create/index.ts, after a successful RSVP insert, call the Resend API to send a confirmation email to the guest_email (or the auth user's email). Use RESEND_API_KEY from Deno.env.get('RESEND_API_KEY'). Email subject: 'You're going to [event.title]!'. Body: event title, formatted date using the event.timezone, location, and a calendar download link (https://{Deno.env.get('SITE_URL')}/functions/v1/ics-export?event_id={event_id}).

2. Create supabase/functions/send-reminders/index.ts — a function that queries all published events where starts_at is between now() + interval '23 hours' and now() + interval '25 hours', finds all rsvps with status='going', and sends each one a reminder email via Resend. Schedule this function to run every hour using pg_cron: CREATE EXTENSION IF NOT EXISTS pg_cron; SELECT cron.schedule('send-reminders', '0 * * * *', $$ SELECT net.http_post(url:='https://[your-project-ref].supabase.co/functions/v1/send-reminders', headers:=jsonb_build_object('Authorization', 'Bearer ' || current_setting('app.service_role_key'))) $$);

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

When to use: Once RESEND_API_KEY and SITE_URL are added to Cloud → Secrets and your Resend domain is verified with SPF + DKIM records

3

Add recurring events with basic RRULE support

Recurring events with client-side virtual expansion, no DB row duplication

~50 credits
prompt
Add basic weekly and monthly recurring event support:

1. Add a column to events: recurrence_rule (text nullable) — stores simple RRULE strings like 'FREQ=WEEKLY;BYDAY=MO' or 'FREQ=MONTHLY;BYMONTHDAY=1'.

2. In the EventForm.tsx organizer editor, add a Recurrence section after the date fields: a toggle 'Repeating event', and if on, a Select with options: Weekly (every week on the same weekday), Monthly (same date each month), None. Write the appropriate RRULE string to the recurrence_rule column.

3. In the Events.tsx public browse page, when generating the events list for display, if an event has a recurrence_rule, expand virtual instances for the next 3 months in JavaScript (do not duplicate rows in the DB). Each virtual instance reuses the original event's data but with computed starts_at/ends_at. Show a 'Repeating' badge on recurring event cards.

4. In EventDetail.tsx for recurring events, show 'Next occurrences' — list the next 5 dates.

Do not duplicate event rows in the database. Virtual expansion only happens at query/render time.

When to use: Only if you have weekly meetups or regular series — skip entirely for one-off event calendars

4

Add a public organizer page

Public organizer profile page with event grid and email follow subscription

~30 credits
prompt
Create a public organizer profile page at /o/[organizer-slug]:

1. Add a slug column to profiles: slug (text unique). In the sign-up flow, auto-generate slug from full_name (lowercase, spaces to dashes, append short uuid suffix for uniqueness).

2. Create src/pages/OrganizerProfile.tsx — shows the organizer's name, a bio (add bio text field to profiles), and a grid of all their published events sorted by starts_at. Use the same EventCard component as the browse page.

3. Add a 'Follow by email' form at the bottom of the organizer profile: input for email + Subscribe button. Store subscribers in a new table: organizer_followers (id uuid pk, organizer_id uuid fk auth.users, follower_email text, subscribed_at timestamptz default now(), unique (organizer_id, follower_email)). RLS: public INSERT only (no SELECT for anon), organizer SELECT for own followers.

4. Add a 'View my public page' link in the OrganizerLayout sidebar pointing to /o/[current-user's-slug].

The organizer_followers table is insert-only for anon — do not add a SELECT policy for anon.

When to use: Once you have 3+ active organizers and want to give each a shareable profile page

5

Add RSVP waitlist when capacity is full

Waitlist status for full events, automatic promotion email when a spot opens up

~40 credits
prompt
Extend the RSVP system to support a waitlist when an event is at capacity:

1. Add 'waitlist' to the rsvps status check constraint: status text check (status in ('going','maybe','no','waitlist')).

2. Update supabase/functions/rsvp-create/index.ts: if the capacity check finds rsvp_count >= capacity, do NOT return 409. Instead, insert the RSVP with status='waitlist' and return 200 { ok: true, status: 'waitlist', position: waitlist_position }. Calculate waitlist position as COUNT(*) FROM rsvps WHERE event_id = $1 AND status = 'waitlist'.

3. Update RSVPDialog.tsx to show 'Join Waitlist' button text when the event detail page shows capacity as full (rsvp_count >= capacity). After a waitlist RSVP, show 'You are #N on the waitlist'.

4. Create supabase/functions/promote-from-waitlist/index.ts — called when a 'going' RSVP is cancelled (status changed to 'no'). It finds the first waitlist RSVP for that event (oldest responded_at), promotes it to status='going', and sends a promotion email via Resend.

5. In the Organizer Dashboard, show a 'Waitlist: N' count next to the RSVP count for each event.

When to use: Only when you are actually hitting capacity on events — most community calendars don't need this until events are selling out

6

Add RSVP list CSV export for organizers

One-click CSV download of the RSVP list for any event, with name/email/status columns

~20 credits
prompt
Add CSV export of RSVPs to the organizer dashboard:

1. In src/pages/organizer/Dashboard.tsx, add an 'Export RSVPs' button on each event row. When clicked, it should fetch all RSVPs for that event (organizer SELECT policy allows this) and generate a CSV file in the browser.

2. The CSV should have columns: Name, Email, RSVP Status, Responded At. For signed-in attendees, join to profiles to get the name and email.

3. Use the browser's Blob API to download the file: create a Blob with type 'text/csv', create an object URL, click a hidden anchor element to download. File name: '[event-slug]-rsvps.csv'.

4. Show a count 'N going / N maybe / N waitlist' summary row before the export button.

No new edge functions needed — use the existing Supabase JS client with the organizer's auth session, which already has SELECT permission for their events' RSVPs.

When to use: Before any in-person event where you need to check guests in at the door or share the list with a venue

Common errors

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

new row violates row-level security policy for table "rsvps"

The rsvps table has no anon or authenticated INSERT policy in the starter — inserts go through the edge function which uses the service-role key. A direct Supabase JS insert from the browser will always fail by design.

Fix — paste into Lovable Agent Mode
Do not add a client-side INSERT policy on rsvps. Instead, confirm RSVPDialog.tsx is calling POST /functions/v1/rsvp-create and NOT calling supabase.from('rsvps').insert() directly. The edge function uses the service-role key internally and bypasses RLS. If you see this error from the edge function itself, check that it is initializing the Supabase client with Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') not the anon key.

Manual fix

In RSVPDialog.tsx, replace any supabase.from('rsvps').insert() call with: const res = await fetch('/functions/v1/rsvp-create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ event_id, guest_name, guest_email, status: 'going' }) })

event shows at wrong time after publish (organizer in New York sees event as 3 hours off)

The EventForm combined the date+time input with JavaScript's new Date() instead of using date-fns-tz zonedTimeToUtc — so the event was stored in the browser's local TZ offset, not the selected IANA timezone.

Fix — paste into Lovable Agent Mode
In EventForm.tsx, update the submit handler so it converts the user's input using date-fns-tz. Replace the raw date construction with: import { zonedTimeToUtc } from 'date-fns-tz'; const startsAtUtc = zonedTimeToUtc(combinedDateTimeString, selectedTimezone); then write startsAtUtc.toISOString() to the starts_at column. On read in EventDetail.tsx and EventCard.tsx, use formatInTimeZone(event.starts_at, event.timezone, 'PPP p zzz') from date-fns-tz.
duplicate key value violates unique constraint "rsvps_event_id_user_id_unique"

A signed-in user submitted the RSVP form twice. The unique constraint on (event_id, user_id) correctly rejected the second insert.

Fix — paste into Lovable Agent Mode
In supabase/functions/rsvp-create/index.ts, change the INSERT to an upsert: INSERT INTO rsvps (event_id, user_id, guest_email, guest_name, status) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (event_id, user_id) WHERE user_id IS NOT NULL DO UPDATE SET status = EXCLUDED.status, responded_at = now() RETURNING *. This way re-submitting changes the status (e.g. 'going' to 'maybe') instead of erroring.
Failed to fetch /functions/v1/rsvp-create — TypeError: Failed to fetch (CORS error in browser console)

The rsvp-create edge function was created without CORS preflight handling. Browsers send an OPTIONS preflight before the actual POST, and without the CORS response the browser blocks the request.

Fix — paste into Lovable Agent Mode
In supabase/functions/rsvp-create/index.ts, add this as the very first check inside the handler: if (req.method === 'OPTIONS') { return new Response('ok', { headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization,content-type' } }); } Also add 'Access-Control-Allow-Origin': '*' to the headers of every other Response you return in the function.
capacity counter shows stale RSVP count after submitting RSVP

EventDetail.tsx fetches the RSVP count once on mount but does not subscribe to Realtime updates on the rsvps table, so the count only refreshes on full page reload.

Fix — paste into Lovable Agent Mode
In EventDetail.tsx, after the initial RSVP count fetch, add a Supabase Realtime subscription: const channel = supabase.channel('rsvps-' + event.id).on('postgres_changes', { event: '*', schema: 'public', table: 'rsvps', filter: 'event_id=eq.' + event.id }, () => { refetchRsvpCount(); }).subscribe(); Clean up with channel.unsubscribe() in the useEffect return. Also enable Realtime on the rsvps table in Cloud → Database → Realtime by toggling it on.

Cost reality

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

Recommended Lovable plan

Pro $25/mo recommended — the starter + capacity edge function + email follow-ups will exceed Free's 30-credit monthly cap before you finish the second iteration cycle

Monthly run cost breakdown

~100–150 credits including starter (~40), CORS/idempotency fix (~15), email reminders (~40), ICS export (~20), plus 2–3 fix rounds on the edge function total
ItemCost
Lovable Pro

Only during active build — cancel after you stop iterating

$25/mo
Supabase (Lovable Cloud)

Free tier handles events + RSVPs well past 500K total rows

$0
Resend

Free tier 3K emails/mo; bump to Pro $20/mo when you exceed ~1.5K RSVPs + reminders per month

$0–$20/mo
Custom domain

Via Lovable Publish → Custom Domain on any paid plan

~$12/yr

Scaling notes: Resend free tier (3K emails/mo) is the first thing to break — it runs out when you send confirmation + reminder to ~1.5K RSVPs/mo. Supabase Free 500MB DB handles ~500K total RSVP rows comfortably. The calendar itself stays cheap until you add recurring event expansion, which generates more query load.

Production checklist

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

Domain & SSL

  • Connect custom domain

    Top-right Publish button → Custom domain tab → add your domain → update DNS CNAME to Lovable's target → verify propagation with dig CNAME yourdomain.com

  • Update SITE_URL secret

    After domain is live, Cloud tab → Secrets → edit SITE_URL to your custom domain — otherwise email links in reminders point to the old lovable.app URL

Email

  • Verify Resend sending domain

    Resend dashboard → Domains → Add Domain → copy SPF and DKIM records → add them in your DNS provider → wait for green status. Without this, all emails land in spam or bounce.

  • Test end-to-end reminder flow

    Create a test event starting in 24h, submit an RSVP with a real email, wait for the pg_cron job to run (or invoke send-reminders function manually from Cloud → Edge Functions → Invoke). Check spam folder if not received.

Backups

  • Confirm daily backup schedule

    Cloud tab → Database → Backups — verify a backup ran today. Lovable Cloud takes daily backups retained ~14 days. For critical event data, export a CSV of events and rsvps monthly via the organizer dashboard.

Monitoring

  • Check edge function error logs before launch

    Cloud tab → Logs → Edge Functions — look for any errors from rsvp-create (CORS, capacity, auth). Resolve before promoting to production audience.

  • Test capacity guard with two browser windows

    Open two incognito windows simultaneously, set event capacity to 1, RSVP in both at the same time. Only one should succeed; the other should receive a 409 capacity_full error and show 'Event is full' in the dialog.

Frequently asked questions

Can I sell tickets with Stripe through this build?

Not in the starter prompt — this kit is for free RSVP events like Luma-style community gatherings. To add paid tickets, you need to add Stripe Checkout to the RSVP flow: create a checkout session edge function, redirect guests to Stripe, and confirm payment in a webhook before inserting the RSVP. That is a follow-up project, roughly equivalent to the shopping cart prompt kit. If you just need paid events for a few events per month, Luma's 2.5% paid-event fee is often cheaper than the dev cost of Stripe integration.

How do I handle recurring weekly meetups without duplicating rows?

The recurring events follow-up prompt adds a recurrence_rule column to the events table and expands virtual instances in JavaScript at render time — no duplicate rows in the database. The starter prompt intentionally omits recurrence because Lovable's first pass on RRULE logic tends to create duplicate rows, which are harder to clean up than a missed virtual expansion. Add recurrence only after the base calendar is working.

Why does my event show the wrong time to attendees in other timezones?

Your event's starts_at is stored as UTC (correct), but you're rendering it without timezone conversion. In EventCard.tsx and EventDetail.tsx, use formatInTimeZone(event.starts_at, event.timezone, 'PPP p zzz') from date-fns-tz — this formats the event in the organizer's stated timezone and appends the timezone abbreviation (e.g. 7:00 PM PDT). If you want to also show the attendee's local time, add a second line: formatInTimeZone(event.starts_at, Intl.DateTimeFormat().resolvedOptions().timeZone, 'h:mm a zzz') + ' your time'.

Can attendees RSVP without signing up for an account?

Yes. The rsvp-create edge function accepts guest_email and guest_name for anonymous RSVPs. The RSVP form in RSVPDialog.tsx shows just two fields (name and email) for unauthenticated visitors, and a one-click confirm for signed-in users. The edge function inserts with user_id = null and guest_email = provided email. Organizers see both types in the RSVP list. Anonymous RSVPs still receive confirmation and reminder emails via Resend as long as they provide a valid email.

How do I send a calendar invite (.ics) by email?

The ics-export edge function generates a VCALENDAR/VEVENT block for any event UUID. In the RSVP confirmation email (sent by rsvp-create), include a line with a link to /functions/v1/ics-export?event_id=UUID. You can also attach the .ics directly in the Resend email body using a data URL — construct the VCALENDAR string server-side in the rsvp-create function, base64-encode it, and add it as an attachment to the Resend API call. The Resend confirmation email follow-up prompt covers this.

What happens when capacity is reached — does it auto-close RSVPs?

After you add the waitlist follow-up prompt, the system switches from rejecting RSVPs when capacity is full to placing them on a waitlist with status='waitlist'. Before that, the rsvp-create edge function returns a 409 capacity_full response and RSVPDialog shows an 'Event is full' message. The capacity counter on EventDetail.tsx shows 'X going / Y capacity' in real time (with the Realtime subscription from the CORS follow-up prompt). The 'RSVP' button becomes disabled when rsvp_count >= capacity so users can't even open the dialog.

Can I export the RSVP list to CSV for the venue?

Yes — the CSV export follow-up prompt adds a one-click download to the organizer dashboard. It fetches all RSVPs for an event using the organizer's auth session (which already has SELECT permission), builds a CSV with Name, Email, Status, and Responded At columns, and triggers a browser download. No new edge function is needed for this — it runs entirely in the browser with the existing Supabase JS client. The generated file is named [event-slug]-rsvps.csv.

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.