# How to Add an Event Calendar to Your App (Copy-Paste Prompts Included)

- Tool: App Features
- Last updated: July 2026

## TL;DR

An event calendar needs a grid UI (FullCalendar.js), a Supabase events table with recurrence rules, and an RSVP Edge Function that enforces capacity server-side. With Lovable or V0 you can ship a working calendar in 3–6 hours for $0–$30/month. Costs start only when Supabase Pro is needed at around 1,000 users or when RSVP email volume exceeds Resend's free tier.

## What an Event Calendar Feature Actually Is

An event calendar lets visitors browse, filter, and register for upcoming events directly inside your app — no third-party ticketing link required. The feature covers three layers: a grid UI that renders month, week, and day views; a database of events with recurrence rules (so a weekly standup does not require 52 manual entries); and an RSVP engine that enforces seat capacity and sends confirmation emails. The product decisions that matter are: who can create events (organizers only, or anyone?), how recurring events expand without blowing up your database, and whether RSVPs are anonymous or authenticated. Getting those three right on the first build is the difference between a calendar that impresses and one that gets replaced.

## Anatomy of the Feature

Five components, two of which AI tools consistently get wrong without explicit prompting: the recurrence engine and the RSVP capacity check.

- **Calendar grid UI** (ui): FullCalendar.js (open-source, 18K GitHub stars) renders month, week, and day views with built-in drag-and-drop event editing. A shadcn/ui Dialog wraps the event detail popover when a user clicks an event tile.
- **Event data layer** (data): A Supabase table named events stores id, title, start_at, end_at, category, description, recurrence_rule, max_capacity, and created_by. A Supabase Realtime subscription keeps the calendar live when an organizer edits an event while viewers are on the page.
- **Recurrence engine** (backend): rrule.js parses RFC 5545 RRULE strings (e.g. FREQ=WEEKLY;BYDAY=MO,WE,FR) stored in the database and expands them into individual occurrences on the frontend at render time.
- **RSVP and registration engine** (backend): A Supabase Edge Function handles RSVP writes: it checks current RSVP count against max_capacity inside a transaction before inserting into the rsvps table, then calls Resend to send a confirmation email to the attendee.
- **Auth gate** (service): Supabase Auth (email magic link or OAuth) differentiates organizers from viewers. Row-level security policies restrict INSERT, UPDATE, and DELETE on the events table to the row's created_by user, while SELECT is open to all.

## Data model

Two tables cover events and RSVPs with row-level security. Run this in the Supabase SQL editor — it is copy-paste ready.

```sql
create table public.events (
  id uuid primary key default gen_random_uuid(),
  title text not null,
  start_at timestamptz not null,
  end_at timestamptz not null,
  category text not null default 'general',
  description text,
  recurrence_rule text,
  max_capacity int,
  created_by uuid references auth.users(id) on delete cascade not null,
  created_at timestamptz not null default now()
);

alter table public.events enable row level security;

create policy "Events are viewable by everyone"
  on public.events for select
  using (true);

create policy "Organizers can insert their own events"
  on public.events for insert
  with check (auth.uid() = created_by);

create policy "Organizers can update their own events"
  on public.events for update
  using (auth.uid() = created_by);

create policy "Organizers can delete their own events"
  on public.events for delete
  using (auth.uid() = created_by);

create index events_start_at_idx on public.events (start_at);
create index events_created_by_idx on public.events (created_by);

create table public.rsvps (
  id uuid primary key default gen_random_uuid(),
  event_id uuid references public.events(id) on delete cascade not null,
  user_id uuid references auth.users(id) on delete cascade not null,
  created_at timestamptz not null default now(),
  unique (event_id, user_id)
);

alter table public.rsvps enable row level security;

create policy "Users can view own RSVPs"
  on public.rsvps for select
  using (auth.uid() = user_id);

create policy "Users can RSVP to events"
  on public.rsvps for insert
  with check (auth.uid() = user_id);

create policy "Users can cancel own RSVPs"
  on public.rsvps for delete
  using (auth.uid() = user_id);

create index rsvps_event_id_idx on public.rsvps (event_id);
create index rsvps_user_id_idx on public.rsvps (user_id);
```

The unique constraint on (event_id, user_id) prevents one user from RSVPing twice. Total capacity overflow is prevented in the Edge Function — the DB constraint alone does not protect against two different users simultaneously exceeding capacity.

## Build paths

### Lovable — fit 4/10, 3–5 hours

Strong all-round path: Lovable's Supabase Cloud integration provisions the database, and it generates FullCalendar plus Edge Function RSVP logic well. Best when you want auth, calendar, and email confirmation in one project.

1. Create a new Lovable project and connect Lovable Cloud so the events and rsvps tables, auth, and Edge Functions are provisioned automatically
2. Paste the prompt below into Agent Mode and let it build the calendar page, event detail modal, organizer dashboard, and RSVP Edge Function
3. Open the published URL on a real device — FullCalendar drag handles and OAuth RSVP flows will not work in the Lovable preview iframe
4. Test RSVP capacity enforcement by filling a low-capacity test event with two separate accounts simultaneously to confirm the Edge Function blocks overflow
5. Connect Resend in the Cloud tab under Secrets to activate confirmation emails for each RSVP

Starter prompt:

```
Build an event calendar feature using FullCalendar.js. Create a Supabase table called events with columns: id (uuid), title (text), start_at (timestamptz), end_at (timestamptz), category (text, values: workshop, meetup, webinar, social), description (text), recurrence_rule (text nullable), max_capacity (int nullable), created_by (uuid FK to auth.users). Also create an rsvps table: id, event_id FK, user_id FK, created_at, with a unique constraint on (event_id, user_id). Calendar page: render FullCalendar in month view with buttons to switch to week and day views. Color-code events by category (workshop=blue, meetup=green, webinar=purple, social=orange). Parse recurrence_rule with rrule.js client-side to expand recurring events at render time using event_id+date as composite key to prevent duplication. On event click: open a shadcn/ui Dialog showing title, date/time in the viewer's local timezone using date-fns-tz, description, organizer name, and remaining capacity. Include a RSVP button for authenticated users. RSVP logic: write a Supabase Edge Function that checks current RSVP count against max_capacity before inserting — return 409 if full; on success send a Resend confirmation email. Show waitlist message when event is full. Past events: grey them out and keep visible. Empty state for months with no events: show a friendly message. Organizer dashboard (auth-gated): create event form with all fields including recurrence selector (none/daily/weekly/monthly). Store all times as UTC timestamptz.
```

Limitations:

- FullCalendar drag-and-drop event editing fails in the Lovable preview iframe — test only on the published URL
- OAuth login (Google, GitHub) for RSVP flows is also blocked in the preview iframe
- Complex custom RRULE strings (e.g. every 3rd Tuesday) may need a follow-up prompt to generate the recurrence UI correctly

### V0 — fit 4/10, 4–6 hours

Best when the calendar is part of a larger Next.js marketing or community site. V0 generates clean Server Component shells for the public event list with ISR caching, and the interactive calendar renders in a Client Component.

1. Prompt V0 with the spec below to generate the calendar page and event modal components
2. Add NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY in the V0 Vars panel
3. Run the SQL schema from this page in the Supabase SQL editor
4. Confirm the FullCalendar import is wrapped in next/dynamic with ssr:false before publishing — the V0 preview will error without this
5. Publish and test the RSVP flow on the live HTTPS URL

Starter prompt:

```
Build a Next.js event calendar feature. Use FullCalendar.js wrapped in next/dynamic with { ssr: false } because it requires browser globals. Supabase tables: events (id uuid, title text, start_at timestamptz, end_at timestamptz, category text, description text, recurrence_rule text nullable, max_capacity int nullable, created_by uuid) and rsvps (id uuid, event_id FK, user_id FK, unique on event_id+user_id). Page structure: Server Component at /events fetches upcoming events from Supabase and passes them as props to a CalendarClient Client Component. CalendarClient renders FullCalendar; parse recurrence_rule with rrule.js client-side. Color-code by category. On event click: open shadcn/ui Dialog with event details, remaining capacity, and RSVP button. RSVP posts to /api/events/rsvp route handler which checks capacity server-side and inserts into rsvps table. Store all timestamps as UTC; display in user's local timezone using date-fns-tz. Organizer create-event form at /events/new (auth-gated via Supabase Auth). Empty month state: 'No events scheduled — check back soon.' Past events: render with 50% opacity. ISR: revalidate the Server Component every 60 seconds.
```

Limitations:

- FullCalendar must be wrapped in next/dynamic with ssr:false — omitting this causes a 'window is not defined' build failure
- Supabase Realtime for live event updates requires a Client Component subscription; V0 may initially generate this in a Server Component
- V0 does not provision the Supabase database — run the SQL schema manually in the Supabase dashboard

### Custom — fit 5/10, 1–2 weeks

Recommended when the calendar is the core product feature. Custom development adds iCal/Google Calendar two-way sync, multi-timezone organizer support, paid RSVP via Stripe, and white-labeling for multiple clients.

1. Full FullCalendar.js integration with custom recurrence UI and iCal .ics file export for each event
2. Google Calendar API two-way sync: import organizer's Google Calendar events and push confirmed RSVPs back to attendee Google Calendar
3. Stripe Payment Element at RSVP time for paid events — webhooks finalize RSVP only after payment confirmed
4. Multi-organizer permission model with approval workflows for community-submitted events

Limitations:

- Two-way Google Calendar sync requires OAuth consent screen review — Google review takes 4–6 weeks for sensitive scopes
- Paid RSVP via Stripe adds refund handling complexity and Stripe account verification requirements

## Gotchas

- **FullCalendar crashes Next.js with 'window is not defined'** — FullCalendar reads browser globals (window, document, navigator) on import. When Next.js renders a Server Component or runs static generation, there is no browser environment — the import throws immediately and breaks the build or causes a runtime error on the server. Fix: Wrap the FullCalendar import in next/dynamic with { ssr: false }. This defers the import to the browser only. Apply this to any component file that directly imports from @fullcalendar/react or any @fullcalendar/* package.
- **Recurring events duplicate on every re-render** — AI tools commonly expand recurring events by running rrule.js inside a useEffect that re-fires on every state change, generating new Date objects each time. React's reconciliation treats these as new items, causing the calendar to remount event tiles and show duplicate counts. Fix: Expand occurrences once using useMemo, keying each expanded event by event_id + ISO date string (e.g. 'evt_abc123_2026-07-14'). FullCalendar uses the event id field for stable rendering — always set it to the composite key.
- **RSVP race condition lets capacity overflow** — The most common AI-generated RSVP flow is: fetch RSVP count client-side, check against max_capacity in React, then call INSERT if capacity allows. Two users submitting RSVP simultaneously both pass the client-side check before either row commits to the database. Fix: Move the capacity check into a Supabase Edge Function that runs SELECT COUNT then INSERT inside a single transaction. Return a 409 Conflict error to the second user. Alternatively, enforce a CHECK constraint in Postgres combined with an application-level retry, though the Edge Function gives a cleaner error message to the user.
- **Events display at wrong time for international users** — When datetimes are stored as plain text strings or as timestamps without timezone offset, and displayed as-is, a 2:00 PM New York event appears as 2:00 PM to a user in London — 5 hours off. This is the single most common complaint in community event apps after launch. Fix: Always store start_at and end_at as timestamptz in UTC. On the frontend, use date-fns-tz to convert to the viewer's local timezone: utcToZonedTime(event.start_at, Intl.DateTimeFormat().resolvedOptions().timeZone).
- **OAuth login for RSVP fails inside Lovable preview** — The Lovable preview runs in a sandboxed iframe that blocks third-party cookies and cross-origin popups. When users click 'RSVP with Google', the OAuth popup is blocked or the redirect fails to return to the correct origin, leaving the user stuck at the provider's login page. Fix: Test all RSVP and auth flows exclusively on the published Lovable URL, not in the preview. The preview is safe for layout and visual QA only — authentication requires the published HTTPS environment.

## Best practices

- Always expand recurring events at render time using rrule.js — never write individual rows to the database for each occurrence, or you will have hundreds of orphaned rows when an organizer edits the rule
- Enforce RSVP capacity server-side in a Supabase Edge Function — a race condition at capacity limit is a support nightmare that erodes trust
- Store all event times as UTC timestamptz and convert to the viewer's local timezone on the frontend using date-fns-tz
- Wrap FullCalendar in next/dynamic with ssr:false in any Next.js project — it is a browser-only library and will break SSR builds without this guard
- Color-code event categories with a fixed palette defined in the database — give organizers a predefined set of colors, not a free-form picker, for visual consistency
- Add a Supabase Realtime subscription so organizers can edit events while viewers see updates instantly without a page refresh
- Grey out past events rather than hiding them — users frequently look back to reference past events, organizers, and attendance before deciding to RSVP to the next edition
- Send RSVP confirmation emails with an .ics attachment so attendees can add the event to their personal calendar with one click from any email client

## Frequently asked questions

### Can I add a calendar without a database?

For a purely read-only calendar with hardcoded or CMS-managed events, yes — FullCalendar renders from a static JSON array. The moment you add RSVPs, capacity limits, recurring events with edit support, or organizer accounts, you need a database. Supabase free tier handles all of this at zero cost until you exceed 500MB of data.

### How do I handle recurring events?

Store the RRULE string in a single database row (e.g. FREQ=WEEKLY;BYDAY=MO). At render time, use rrule.js on the frontend to expand it into individual occurrence dates and feed them to FullCalendar. This way editing the recurrence rule updates all future occurrences in one database write, not hundreds of individual rows.

### Can users add events to Google Calendar from mine?

Yes, with two approaches: the simple path is generating an .ics file that any calendar app opens — Resend can attach it to the RSVP confirmation email. The advanced path uses the Google Calendar API to add the event directly to the user's Google Calendar after OAuth consent. The .ics approach takes 30 minutes to implement; the Google Calendar API takes 2–4 hours and requires a Google Cloud project.

### What's the difference between FullCalendar and a custom-built grid?

FullCalendar gives you month/week/day views, drag-and-drop editing, timezone support, and recurring event display out of the box — all free and open-source. A custom-built CSS grid takes 2–4 days to reach the same feature parity and typically has edge cases in month boundary rendering and leap year handling. Unless you need pixel-perfect design control or a completely non-standard layout, FullCalendar is the correct choice.

### How do I prevent double-booking for limited-capacity events?

Add a unique constraint on (event_id, user_id) in the rsvps table to prevent one user RSVPing twice. For total capacity, enforce the check in a Supabase Edge Function that runs SELECT COUNT then INSERT in a transaction — never trust a client-side capacity check, as two simultaneous clicks can both pass before either commits to the database.

### Can I show events filtered by category?

Yes. FullCalendar has built-in event source filtering. Store a category field in your events table, pass the active filter as a parameter to your Supabase query, and update FullCalendar's event source when the filter changes. A category pill filter bar above the calendar is a well-understood UX pattern that most users find immediately.

### How do I send event reminder emails?

At RSVP time, use a Supabase Edge Function to calculate the time 24 hours before the event and schedule a Resend email. For higher reliability, use pg_cron (available on Supabase Pro) to run a nightly job that queries rsvps where event start_at is within 24 hours and sends batch Resend emails. The pg_cron approach is more reliable for high volume because it does not depend on a background task staying alive.

### Can I embed this calendar in an iframe on another site?

Yes — publish the app and embed the /events URL in an iframe on your marketing site. Note that OAuth flows inside an iframe break for the same reason they break in the Lovable preview — users will need to authenticate in the main window before the iframe calendar picks up their session, or open the calendar in a new tab for registration.

---

Source: https://www.rapidevelopers.com/app-features/event-calendar
© RapidDev — https://www.rapidevelopers.com/app-features/event-calendar
