Build a full conference management system in Lovable covering the complete event lifecycle: a call-for-proposals submission portal, speaker profile management, a multi-track schedule builder with drag-and-drop session ordering, Stripe-powered attendee registration with ticket tiers, and conflict validation to prevent double-booking the same room or speaker. Build time is approximately 4 hours.
What you're building
Conference management has three distinct phases: pre-conference (CFP and speaker management), planning (schedule building), and execution (attendee registration and on-site check-in). This build covers all three.
The CFP system is a public submission portal. Speakers fill out a form with their talk title, abstract (300–1000 words), format (talk/workshop/panel/lightning), preferred duration, and speaker bio. Submissions go into a pending queue. Organizers review each proposal and move it to accepted, rejected, or waitlisted. Accepted proposals automatically create speaker records and can be added to the schedule.
The schedule builder uses dnd-kit for drag-and-drop. Tracks are rendered as columns (e.g. Main Stage, Track A, Track B). Each track column contains time slots as rows. Sessions can be dragged between tracks or within a track to reorder. Before any drop operation completes, a constraint validator checks whether the speaker is already speaking in another session at the same time, and whether the room is already occupied. If a conflict is detected, the drop is rejected and an error badge appears on the conflicting session.
Stripe integration follows the standard Lovable pattern: paid ticket registration calls an Edge Function that creates a Stripe Checkout session. The Checkout success URL points back to the conference site. A Stripe webhook Edge Function handles payment confirmation and registers the attendee.
Final result
A complete conference platform with CFP, schedule management with conflict detection, Stripe ticket payments, and an attendee-facing schedule page.
Tech stack
Prerequisites
- Lovable Pro account for multi-file Edge Function generation
- Supabase project with Auth enabled for organizer accounts
- Stripe account with test mode API keys (publishable and secret)
- STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, and SUPABASE_SERVICE_ROLE_KEY in Cloud tab → Secrets
- Optional: Resend account for speaker acceptance and registration confirmation emails
- Basic understanding of Stripe Checkout and webhook event handling
Build steps
Create the conference schema
Prompt Lovable to generate the full database schema. This is a large schema — take it in one prompt so Lovable generates consistent foreign key references and RLS across all tables.
1Create a Supabase schema for a conference management system.23Tables:4- conferences: id (uuid pk), organizer_id (references auth.users), name (text), tagline (text), start_date (date), end_date (date), venue (text), city (text), website_url (text), logo_url (text), cfp_open (bool default false), cfp_deadline (timestamptz), is_published (bool default false), created_at56- ticket_tiers: id (uuid pk), conference_id (references conferences), name (text), description (text), price_cents (int), total_quantity (int), remaining_quantity (int), sale_start (timestamptz), sale_end (timestamptz), sort_order (int)78- cfp_proposals: id (uuid pk), conference_id (references conferences), speaker_name (text), speaker_email (text), speaker_bio (text), speaker_company (text), speaker_photo_url (text), talk_title (text), talk_abstract (text), talk_format (text check: talk|workshop|panel|lightning), preferred_duration_minutes (int), technical_level (text check: beginner|intermediate|advanced), tags (text array), status (text default 'pending' check: pending|accepted|rejected|waitlisted), organizer_notes (text), submitted_at (timestamptz default now())910- speakers: id (uuid pk), conference_id (references conferences), proposal_id (references cfp_proposals), name (text), email (text), bio (text), company (text), photo_url (text), website_url (text), twitter_handle (text), linkedin_url (text)1112- tracks: id (uuid pk), conference_id (references conferences), name (text), color (text), room (text), sort_order (int)1314- sessions: id (uuid pk), conference_id (references conferences), track_id (references tracks), speaker_id (references speakers nullable), title (text), description (text), format (text), duration_minutes (int), scheduled_start (timestamptz nullable), scheduled_end (timestamptz nullable), room (text), status (text default 'draft' check: draft|confirmed|cancelled), sort_order (int)1516- attendees: id (uuid pk), conference_id (references conferences), ticket_tier_id (references ticket_tiers), name (text), email (text), company (text), registration_code (text unique), stripe_payment_intent_id (text), status (text default 'confirmed' check: confirmed|cancelled), checked_in (bool default false), checked_in_at (timestamptz), created_at1718RLS:19- conferences: SELECT public; INSERT/UPDATE where organizer_id = auth.uid()20- ticket_tiers/tracks/sessions: SELECT public; INSERT/UPDATE/DELETE where conference_id IN (SELECT id FROM conferences WHERE organizer_id = auth.uid())21- cfp_proposals: SELECT where conference_id IN organizer's conferences OR speaker_email = auth.jwt()->>'email'; INSERT public22- speakers: SELECT public; INSERT/UPDATE/DELETE organizer only23- attendees: SELECT where conference_id in organizer's conferences; INSERT by SECURITY DEFINER function only2425Create a function register_attendee(p_conference_id uuid, p_tier_id uuid, p_name text, p_email text, p_company text, p_payment_intent_id text) RETURNS jsonb SECURITY DEFINER that atomically decrements remaining_quantity and inserts the attendee row with a generated registration_code.Pro tip: Ask Lovable to generate the TypeScript types file immediately after the schema creation: 'Run supabase gen types and write the result to src/types/database.types.ts'. Use these types throughout the build to catch mismatches early.
Expected result: All eight tables are created with appropriate constraints and RLS. The register_attendee SECURITY DEFINER function exists. Ticket tier quantity fields have CHECK constraints preventing negative values.
Build the CFP submission portal and organizer review dashboard
Ask Lovable to build both the public CFP submission form and the organizer's review interface. These are two separate pages but tightly coupled through the cfp_proposals table.
1Build two pages:231. Public CFP submission page at src/pages/CFPSubmit.tsx (route /cfp/:conferenceSlug):4- Fetch conference to confirm cfp_open = true and cfp_deadline is in the future5- If CFP closed, show 'Submissions are now closed' message6- Form (react-hook-form + zod):7 - speaker_name, speaker_email, speaker_bio (Textarea 50–500 words), speaker_company8 - speaker_photo upload (Supabase Storage, returns URL)9 - talk_title, talk_abstract (Textarea 300–1000 words with live word count)10 - talk_format: Select (Talk, Workshop, Panel, Lightning Talk)11 - preferred_duration_minutes: Select (15/30/45/60/90)12 - technical_level: RadioGroup (Beginner, Intermediate, Advanced)13 - tags: multi-value Input (e.g. React, TypeScript, Architecture)14- On submit: INSERT into cfp_proposals15- Confirmation: 'Your proposal was submitted. Reference: [proposal id first 8 chars]'16172. Organizer CFP review page at src/pages/CFPReview.tsx (requires auth):18- Show proposal count by status in Badge pills: Pending (N), Accepted (N), Rejected (N), Waitlisted (N)19- Tabs for each status20- Each proposal as a Card:21 - Speaker photo avatar, name, company22 - Talk title (large), format Badge, duration Badge, level Badge23 - Abstract truncated to 3 lines with 'Read more' expand24 - Action Buttons: 'Accept', 'Reject', 'Waitlist', 'View Full'25 - 'View Full' opens a Sheet with complete proposal details and organizer_notes Textarea26- Accept action: UPDATE status='accepted', then INSERT into speakers using proposal data27- Show a progress bar: X of Y proposals reviewed28- Filter by format, level, and tagsPro tip: When a proposal is accepted, automatically create the speaker record AND a draft session in a transaction. Ask Lovable: 'When accepting a proposal, also INSERT a session row with status=draft, title=proposal.talk_title, speaker_id=new speaker id, duration_minutes=proposal.preferred_duration_minutes. Leave scheduled_start as null until the organizer places it in the schedule builder.'
Expected result: Speakers can submit proposals via the public form. Organizers can review proposals by status, read full abstracts, and accept/reject. Accepting a proposal creates a speaker and draft session.
Build the multi-track schedule builder with conflict validation
Ask Lovable to build the schedule builder. Tracks are columns, sessions are cards within each column, and dnd-kit handles drag-and-drop. A constraint validator runs before each drop to check for speaker and room conflicts.
1Build a schedule builder at src/pages/ScheduleBuilder.tsx (requires auth).23Layout:4- Horizontal scroll container with one column per track5- Each column header: track name (colored with track.color), room name, add session Button6- Each column body: list of session Cards sorted by sort_order7- Unscheduled sessions panel on the left: sessions with scheduled_start = null8- Day selector at the top if conference spans multiple days910Drag-and-drop (dnd-kit):11- Use DndContext, SortableContext, useSortable hooks12- Sessions can be dragged from the unscheduled panel into any track column13- Sessions can be reordered within a track column14- Sessions can be moved between track columns1516Time assignment:17- When a session is dropped into a track column at a position, show a time picker popover to assign scheduled_start18- scheduled_end = scheduled_start + session.duration_minutes1920Conflict validation (run before confirming any drop):21function validateDrop(session, targetTrack, proposedStart, allSessions): ValidationResult221. If session has a speaker_id, check if that speaker has another session in allSessions where the time ranges overlap232. If targetTrack has a room, check if another session in the same track/room has overlapping times243. Return { valid: true } or { valid: false, conflict: 'Speaker [name] is already scheduled at this time in Track B' }2526If validation fails:27- Cancel the drop (do not update the database)28- Show a Toast with the conflict message29- Highlight the conflicting session with a red border for 3 seconds3031Session Card component:32- Title, speaker name (if assigned), duration Badge, format Badge33- Status indicator: draft=gray dot, confirmed=green dot34- 'Edit' icon opens session edit Dialog35- Grip handle for drag initiationPro tip: Implement optimistic updates for drag-and-drop. Update the local session state immediately on drop, then persist to Supabase. If the Supabase update fails, revert to the previous state and show an error toast. This makes drag-and-drop feel instant even on slow connections.
Expected result: The schedule builder shows tracks as columns. Sessions drag between tracks and the unscheduled panel. Dropping a session with a conflicting speaker or room shows an error toast and cancels the drop.
Build the Stripe-powered attendee registration
Ask Lovable to build the attendee registration flow with Stripe Checkout for paid tickets and the webhook Edge Function that confirms registrations after payment.
1Build two components:231. Public registration page at src/pages/AttendeeRegistration.tsx:4- Show conference details and ticket tiers as Cards5- Each tier: name, description, price, remaining quantity, sale window6- 'Register' Button disabled if sold out or outside sale window7- Registration form (free tier, no Stripe):8 - name, email, company (optional)9 - Submit calls register_attendee RPC function directly10- Registration flow (paid tier, with Stripe):11 - Collect name, email, company in a form first12 - Submit calls create-checkout Edge Function13 - Redirect to Stripe Checkout14 - Success URL: /conference/[slug]/registration-success?session_id={CHECKOUT_SESSION_ID}15162. Stripe integration Edge Functions:1718supabase/functions/create-checkout/index.ts:19- Receive POST with { tier_id, conference_id, name, email, company }20- Create Stripe Checkout session with:21 - line_items from ticket tier name and price_cents22 - metadata: { tier_id, conference_id, name, email, company }23 - success_url and cancel_url24- Return { url: checkoutUrl }2526supabase/functions/stripe-webhook/index.ts:27- Verify webhook signature using STRIPE_WEBHOOK_SECRET28- Handle checkout.session.completed event29- Extract metadata from session.metadata30- Call register_attendee(conference_id, tier_id, name, email, company, payment_intent_id)31- Return 2003233Registration success page at src/pages/RegistrationSuccess.tsx:34- Fetch attendee by stripe session ID35- Show confirmation: name, ticket tier, registration_code36- QR code of registration_code (qrcode.react)Pro tip: Always verify the Stripe webhook signature before processing. In the Deno Edge Function: import Stripe from 'https://esm.sh/stripe@14'; const event = await stripe.webhooks.constructEventAsync(rawBody, signature, STRIPE_WEBHOOK_SECRET). The constructEventAsync (not constructEvent) is required in async Deno environments.
Expected result: Paid ticket registration redirects to Stripe Checkout. After payment, the webhook Edge Function calls register_attendee and creates the attendee record. The success page shows the registration code and QR code.
Build the public schedule page and organizer attendee management
Ask Lovable to build the published conference schedule that attendees browse and the organizer's attendee management dashboard.
1Build two pages:231. Public schedule page at src/pages/ConferenceSchedule.tsx:4- Only visible when conference.is_published = true5- Filter bar: track Select, format filter Checkboxes, speaker Select6- Layout: time slots as rows, tracks as columns (same as builder but read-only)7- Each session Card shows: title, speaker photo + name, duration, format Badge, room8- Clicking a session opens a Sheet with full details: abstract, speaker bio, room, time9- 'Add to my schedule' Button (stores in localStorage as a Set of session IDs)10- 'My Schedule' tab showing only bookmarked sessions11- Mobile view: single column with track filter instead of multi-column grid12132. Organizer attendee management at src/pages/AttendeeManagement.tsx:14- Stats row: total registrations, checked in count, revenue (sum of ticket prices)15- DataTable of all attendees: name, email, company, ticket tier Badge, registration code, status, checked_in16- Search by name or email17- Export to CSV Button18- Check-in tab: Input for registration code + 'Check In' Button (same pattern as event-registration-system check-in page)19- Manually mark attendee as checked_in = true and set checked_in_at = now()20- Show real-time checked-in count updating as attendees are processedExpected result: The public schedule page shows the full conference schedule with filtering. The organizer attendee dashboard shows registrations, check-in status, and allows manual check-in by code.
Complete code
1type Session = {2 id: string3 track_id: string4 speaker_id: string | null5 speaker_name?: string6 room: string | null7 scheduled_start: string | null8 scheduled_end: string | null9 duration_minutes: number10 title: string11}1213type ConflictResult =14 | { valid: true }15 | { valid: false; conflict: string; conflicting_session_id: string }1617function timeRangesOverlap(18 aStart: string,19 aEnd: string,20 bStart: string,21 bEnd: string22): boolean {23 return aStart < bEnd && aEnd > bStart24}2526export function validateScheduleConflicts(27 session: Session,28 proposedTrackId: string,29 proposedStart: string,30 allSessions: Session[]31): ConflictResult {32 const proposedEnd = new Date(33 new Date(proposedStart).getTime() + session.duration_minutes * 60_00034 ).toISOString()3536 const otherSessions = allSessions.filter(37 (s) => s.id !== session.id && s.scheduled_start !== null && s.scheduled_end !== null38 )3940 // Check speaker conflict41 if (session.speaker_id) {42 const speakerConflict = otherSessions.find(43 (s) =>44 s.speaker_id === session.speaker_id &&45 timeRangesOverlap(46 proposedStart,47 proposedEnd,48 s.scheduled_start!,49 s.scheduled_end!50 )51 )52 if (speakerConflict) {53 return {54 valid: false,55 conflict: `${session.speaker_name ?? 'This speaker'} is already scheduled at this time in another session: '${speakerConflict.title}'`,56 conflicting_session_id: speakerConflict.id,57 }58 }59 }6061 // Check room conflict within the same track62 if (session.room) {63 const roomConflict = otherSessions.find(64 (s) =>65 s.track_id === proposedTrackId &&66 s.room === session.room &&67 timeRangesOverlap(68 proposedStart,69 proposedEnd,70 s.scheduled_start!,71 s.scheduled_end!72 )73 )74 if (roomConflict) {75 return {76 valid: false,77 conflict: `Room '${session.room}' is already occupied by '${roomConflict.title}' at this time`,78 conflicting_session_id: roomConflict.id,79 }80 }81 }8283 return { valid: true }84}Customization ideas
Speaker portal with proposal editing
Add a speaker-facing portal at /speaker/portal where accepted speakers can log in (Supabase Auth with magic link), view their accepted sessions, update their bio and photo, and submit A/V requirements. Speakers receive a magic link email when their proposal is accepted, linked to their email address.
Attendee session bookmarking and personal schedule
Upgrade the localStorage bookmarking to a persistent Supabase attendee_bookmarks table. Registered attendees log in to access a personal schedule page showing their bookmarked sessions in timeline order. Send a personalized schedule PDF via email the day before the conference using an Edge Function.
Live session feedback and ratings
After each session's scheduled end time, registered attendees receive a push notification or email link to a 30-second feedback form: a 1–5 star rating and optional comment. Results appear in the organizer dashboard per session. Speakers can view their aggregate rating after the conference.
Sponsor management and exhibition floor
Add a sponsors table with tier (gold/silver/bronze), logo, description, and booth number. Create a public sponsors page and an exhibition floor map built with SVG showing booth locations. Sponsor representatives get login access to view attendee opt-in contact details they can export after the event.
Multi-day and multi-venue support
Add a day column to sessions (or derive it from scheduled_start) and a venues table. The schedule builder gets a day tab switcher. Tracks can be assigned to specific venues. The public schedule page groups sessions by day and shows venue information on each track column header.
Common pitfalls
Pitfall: Running conflict validation only on the client without re-validating on the server
How to avoid: Add a validate_session_placement(session_id, track_id, starts_at) Postgres function that checks for conflicts against live database data. Call this before persisting any drag-and-drop change. If validation fails server-side, revert the optimistic update and show the conflict error.
Pitfall: Using constructEvent instead of constructEventAsync for Stripe webhooks in Deno
How to avoid: Use stripe.webhooks.constructEventAsync(rawBody, signature, webhookSecret) which uses the Web Crypto API available in both Node.js and Deno. Always read the raw request body as text before constructEventAsync: const body = await req.text().
Pitfall: Not verifying the Stripe webhook signature
How to avoid: Always verify the Stripe-Signature header in the webhook handler using constructEventAsync with your STRIPE_WEBHOOK_SECRET. Reject any request where verification fails with a 400 response.
Pitfall: Building the schedule builder without optimistic updates
How to avoid: Apply the session state change locally immediately when a valid drop occurs. Persist to Supabase in the background. On Supabase error, revert the local state and show an error toast. This makes the schedule builder feel instant.
Pitfall: Granting direct INSERT on the attendees table instead of using the SECURITY DEFINER function
How to avoid: Remove the INSERT RLS policy from attendees entirely. All insertions must go through the register_attendee SECURITY DEFINER function, which validates payment intent and decrements remaining_quantity atomically.
Best practices
- Use a SECURITY DEFINER register_attendee function for all attendee creation. This ensures capacity decrement and attendee insertion are atomic and that no client can bypass payment verification.
- Validate schedule conflicts both client-side (immediate UX feedback) and server-side (via a Postgres validation function before persisting the change). Client-side validation is for speed; server-side is for correctness.
- Store Stripe payment_intent_id on the attendees row to reconcile refunds, disputes, and failed payments in the Stripe dashboard. Query this ID when handling refund events from Stripe webhooks.
- Use dnd-kit's collision detection algorithm appropriate for grid layouts. The rectIntersection or closestCenter strategy works well for multi-column schedule grids. Configure it explicitly rather than using the default.
- Implement optimistic updates for all drag-and-drop operations. Apply state changes locally first, persist to Supabase in background, and revert on failure. This is the difference between a builder that feels fast and one that feels sluggish.
- Always use constructEventAsync (not constructEvent) for Stripe webhook verification in Deno Edge Functions. The async version uses the Web Crypto API instead of Node.js crypto.
- Subscribe to Supabase Realtime on the sessions table in the schedule builder when multiple organizers may edit simultaneously. Broadcast session updates to all connected builders so conflicts from simultaneous edits are visible in real time.
- Scope the public schedule page behind conference.is_published = true. Add a preview link for organizers to see the schedule before publishing. This prevents a half-built schedule from being visible to attendees.
AI prompts to try
Copy these prompts to build this project faster.
I'm building a multi-track conference schedule builder in React with dnd-kit. I have a sessions array and tracks array from Supabase. Sessions can be dragged between tracks and reordered within tracks. Help me design the data model for local state management during drag-and-drop: how should I structure the sessions state so I can efficiently find sessions by track, update sort_order on reorder, move a session to a new track, and validate conflicts before committing a drop? Show me the TypeScript types and the reducer or state update functions.
Add a 'Schedule gap analysis' feature to the schedule builder. For each track, highlight time gaps between sessions that are longer than 30 minutes with a yellow 'Gap: 45 min' placeholder card between them. When the organizer clicks a gap card, offer to insert a break session (coffee break, lunch, networking) from a list of preset break types. This helps organizers spot unintentional scheduling gaps without manually scanning the timeline.
In Supabase, create a view conference_schedule_view that joins sessions, speakers, and tracks to produce a flat row per session suitable for the public schedule page. Include: session id/title/description/format/duration, scheduled_start/end, track name/color/room, speaker name/bio/photo_url/company, session status. Add an RLS policy that only returns sessions WHERE sessions.status = 'confirmed' AND EXISTS (SELECT 1 FROM conferences WHERE id = sessions.conference_id AND is_published = true). This way the public schedule page can fetch from the view without any filtering logic in the client.
Frequently asked questions
Is dnd-kit the right drag-and-drop library for Lovable?
Yes. dnd-kit is the recommended drag-and-drop library for React in 2025. It is accessible, works with touch devices, and has TypeScript support. Ask Lovable to use @dnd-kit/core and @dnd-kit/sortable. Lovable can install npm packages automatically when you reference them in your prompt.
How do I handle the case where two organizers edit the schedule at the same time?
Subscribe to Supabase Realtime on the sessions table in the schedule builder. When another organizer moves a session, the change broadcasts to all connected builders and the session cards update automatically. Before persisting any drop, call the server-side validate_session_placement function to check against live data, not just the local state which may be stale.
Why use constructEventAsync instead of constructEvent for Stripe webhooks?
Supabase Edge Functions run on Deno, which does not have Node.js's crypto module. The synchronous constructEvent depends on that module and throws an error in Deno. The constructEventAsync version uses the Web Crypto API (crypto.subtle), which is available in both Deno and Node.js environments.
How do I handle Stripe refunds when an attendee cancels?
When an organizer cancels an attendee's registration, call a Supabase Edge Function that issues a refund via the Stripe API using the stored stripe_payment_intent_id: await stripe.refunds.create({ payment_intent: attendee.stripe_payment_intent_id }). The Edge Function also sets the attendee status to 'cancelled' and increments remaining_quantity on the ticket tier.
Can I run the CFP without requiring speakers to create an account?
Yes. The public CFP submission form inserts into cfp_proposals without any auth requirement (the INSERT policy allows anon). Speakers get a confirmation email with a reference code. If you want speakers to later edit their proposals or access a speaker portal, implement magic link auth triggered after their proposal is accepted.
How do I prevent the schedule from being visible to attendees before it is finalized?
The conference has an is_published boolean and the public schedule page's RLS policy or client-side check only shows schedules for published conferences. Organizers can toggle is_published when the schedule is ready. Until then, the schedule builder is only accessible to authenticated organizers. Add a 'Preview as Attendee' link in the builder that temporarily shows the organizer the public view.
How large can the conference be before this architecture needs optimization?
The schema and queries in this guide handle conferences up to a few thousand attendees without issues. The main performance consideration is the schedule builder's session query — add an index on sessions(conference_id, track_id) for fast track-based fetches. For attendee management DataTable, add pagination for conferences with more than 500 attendees.
Where can I get help building additional features like a speaker portal or mobile app?
RapidDev builds full production Lovable applications. If you need a speaker portal with magic link auth, a post-event certificate generator, sponsor management, or a companion mobile check-in app, reach out to discuss a scoped build.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation