Skip to main content
RapidDev - Software Development Agency

How to Build Event registration system with V0

Build an event registration system with V0 using Next.js, Stripe for ticket payments, and Supabase for attendee management. You'll create event pages with ticket tiers, secure checkout, QR code generation for check-in, and an organizer dashboard — all in about 1-2 hours without touching a terminal.

What you'll build

  • Event listing page with banner images, dates, and ticket availability using shadcn/ui Card
  • Ticket tier selection with RadioGroup and capacity tracking via Progress bar
  • Stripe Checkout integration with atomic ticket reservation to prevent overselling
  • QR code generation for registration confirmation and event check-in
  • Organizer dashboard with attendee Table, check-in scanner, and sales analytics
  • Waitlist system for sold-out events with automatic notification
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate10 min read1-2 hoursV0 Premium or higherApril 2026RapidDev Engineering Team
TL;DR

Build an event registration system with V0 using Next.js, Stripe for ticket payments, and Supabase for attendee management. You'll create event pages with ticket tiers, secure checkout, QR code generation for check-in, and an organizer dashboard — all in about 1-2 hours without touching a terminal.

What you're building

Event registration systems power conferences, workshops, and community gatherings. Organizers need to create events with multiple ticket tiers, manage capacity, process payments, and check in attendees — all without complex infrastructure.

V0 generates the event pages, registration forms, and organizer dashboard from prompts. Stripe handles payment processing via the Vercel Marketplace integration, and Supabase stores event data with atomic ticket reservation to prevent overselling.

The architecture uses Next.js App Router with Server Components for event listing and detail pages, API routes for Stripe Checkout and webhook handling, a PostgreSQL function for atomic ticket reservation, and client components for the interactive check-in scanner.

Final result

A complete event registration system with ticket tiers, Stripe payments, QR code confirmations, organizer check-in tools, and waitlist management.

Tech stack

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase
StripePayments

Prerequisites

  • A V0 account (Premium plan for Stripe integration iterations)
  • A Supabase project (free tier works — connect via V0's Connect panel)
  • A Stripe account (test mode — add via Vercel Marketplace in V0)
  • An event to register attendees for (conference, workshop, meetup)

Build steps

1

Set up the database schema for events and registrations

Open V0 and create a new project. Connect Supabase and Stripe via the Connect panel and Vercel Marketplace. Create the schema for events, ticket tiers, registrations, and waitlist.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build an event registration system. Create a Supabase schema with:
3// 1. events: id (uuid PK), title (text), description (text), venue (text), start_date (timestamptz), end_date (timestamptz), banner_image_url (text), organizer_id (uuid FK to auth.users), status (text default 'draft'), max_capacity (int), created_at (timestamptz)
4// 2. ticket_tiers: id (uuid PK), event_id (uuid FK to events), name (text), price_cents (int), quantity (int), sold_count (int default 0), description (text)
5// 3. registrations: id (uuid PK), event_id (uuid FK to events), ticket_tier_id (uuid FK to ticket_tiers), user_id (uuid FK to auth.users), attendee_name (text), attendee_email (text), qr_code (text unique), checked_in (boolean default false), checked_in_at (timestamptz), stripe_payment_intent_id (text), created_at (timestamptz)
6// 4. waitlist: id (uuid PK), event_id (uuid FK to events), email (text), created_at (timestamptz)
7// Create a PostgreSQL function reserve_ticket(tier_id uuid) that does: UPDATE ticket_tiers SET sold_count = sold_count + 1 WHERE id = tier_id AND sold_count < quantity RETURNING id
8// Add RLS: anyone can read events, authenticated users can register.

Pro tip: The reserve_ticket function prevents overselling by atomically checking and incrementing sold_count in a single SQL statement.

Expected result: Database schema created with atomic ticket reservation function. Stripe keys auto-provisioned in the Vars tab.

2

Build the event listing and detail pages

Create Server Component pages for browsing events and viewing individual event details with ticket tier selection. The detail page shows capacity, pricing, and a registration form.

app/page.tsx
1import { createClient } from '@/lib/supabase/server'
2import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
3import { Badge } from '@/components/ui/badge'
4import { Progress } from '@/components/ui/progress'
5import Link from 'next/link'
6
7export default async function EventsPage() {
8 const supabase = await createClient()
9
10 const { data: events } = await supabase
11 .from('events')
12 .select('*, ticket_tiers(id, name, price_cents, quantity, sold_count)')
13 .eq('status', 'published')
14 .gte('end_date', new Date().toISOString())
15 .order('start_date')
16
17 return (
18 <div className="container mx-auto py-8">
19 <h1 className="text-4xl font-bold mb-8">Upcoming Events</h1>
20 <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
21 {events?.map((event) => {
22 const totalCapacity = event.ticket_tiers.reduce((s: number, t: any) => s + t.quantity, 0)
23 const totalSold = event.ticket_tiers.reduce((s: number, t: any) => s + t.sold_count, 0)
24 const lowestPrice = Math.min(...event.ticket_tiers.map((t: any) => t.price_cents))
25
26 return (
27 <Link key={event.id} href={`/events/${event.id}`}>
28 <Card className="overflow-hidden hover:shadow-md transition-shadow">
29 {event.banner_image_url && (
30 <img src={event.banner_image_url} alt={event.title} className="w-full h-48 object-cover" />
31 )}
32 <CardHeader>
33 <CardTitle>{event.title}</CardTitle>
34 <p className="text-sm text-muted-foreground">
35 {new Date(event.start_date).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}
36 </p>
37 </CardHeader>
38 <CardContent>
39 <div className="flex items-center justify-between">
40 <Badge variant="secondary">From ${(lowestPrice / 100).toFixed(2)}</Badge>
41 <span className="text-sm text-muted-foreground">{totalSold}/{totalCapacity} registered</span>
42 </div>
43 <Progress value={(totalSold / totalCapacity) * 100} className="mt-2" />
44 </CardContent>
45 </Card>
46 </Link>
47 )
48 })}
49 </div>
50 </div>
51 )
52}

Expected result: The events page shows upcoming events with banner images, dates, pricing, and capacity progress bars.

3

Create the registration API route with atomic ticket reservation

Build the API route that reserves a ticket atomically, creates a Stripe Checkout session, and generates a unique QR code. If payment fails, the reservation is rolled back.

app/api/register/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import Stripe from 'stripe'
3import { createClient } from '@supabase/supabase-js'
4import { randomBytes } from 'crypto'
5
6const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
7const supabase = createClient(
8 process.env.SUPABASE_URL!,
9 process.env.SUPABASE_SERVICE_ROLE_KEY!
10)
11
12export async function POST(req: NextRequest) {
13 const { eventId, tierId, attendeeName, attendeeEmail } = await req.json()
14
15 const { data: reserved } = await supabase.rpc('reserve_ticket', {
16 tier_id: tierId,
17 })
18
19 if (!reserved) {
20 return NextResponse.json({ error: 'Tickets sold out' }, { status: 409 })
21 }
22
23 const { data: tier } = await supabase
24 .from('ticket_tiers')
25 .select('name, price_cents, event_id')
26 .eq('id', tierId)
27 .single()
28
29 const qrCode = randomBytes(16).toString('hex')
30
31 const session = await stripe.checkout.sessions.create({
32 payment_method_types: ['card'],
33 customer_email: attendeeEmail,
34 mode: 'payment',
35 line_items: [{
36 price_data: {
37 currency: 'usd',
38 unit_amount: tier!.price_cents,
39 product_data: { name: `${tier!.name} ticket` },
40 },
41 quantity: 1,
42 }],
43 metadata: { eventId, tierId, attendeeName, attendeeEmail, qrCode },
44 success_url: `${req.nextUrl.origin}/registration/${qrCode}`,
45 cancel_url: `${req.nextUrl.origin}/events/${eventId}`,
46 })
47
48 return NextResponse.json({ url: session.url })
49}

Pro tip: The reserve_ticket RPC uses WHERE sold_count < quantity in the UPDATE, so if the ticket is sold out, no row is returned and we can immediately respond with 409 Conflict.

Expected result: The registration route atomically reserves a ticket, creates Stripe Checkout, and returns the checkout URL. Sold-out tiers return a 409 error.

4

Handle the Stripe webhook and build the confirmation page

Create the webhook that inserts the registration record on successful payment, and build the confirmation page that displays the QR code for event check-in.

app/api/webhooks/stripe/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import Stripe from 'stripe'
3import { createClient } from '@supabase/supabase-js'
4
5const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
6const supabase = createClient(
7 process.env.SUPABASE_URL!,
8 process.env.SUPABASE_SERVICE_ROLE_KEY!
9)
10
11export async function POST(req: NextRequest) {
12 const body = await req.text()
13 const sig = req.headers.get('stripe-signature')!
14
15 let event: Stripe.Event
16 try {
17 event = stripe.webhooks.constructEvent(
18 body, sig, process.env.STRIPE_WEBHOOK_SECRET!
19 )
20 } catch {
21 return NextResponse.json({ error: 'Invalid signature' }, { status: 400 })
22 }
23
24 if (event.type === 'checkout.session.completed') {
25 const session = event.data.object as Stripe.Checkout.Session
26 const { eventId, tierId, attendeeName, attendeeEmail, qrCode } = session.metadata ?? {}
27
28 await supabase.from('registrations').insert({
29 event_id: eventId,
30 ticket_tier_id: tierId,
31 attendee_name: attendeeName,
32 attendee_email: attendeeEmail,
33 qr_code: qrCode,
34 stripe_payment_intent_id: session.payment_intent as string,
35 })
36 }
37
38 return NextResponse.json({ received: true })
39}

Expected result: On successful payment, the webhook inserts the registration with the QR code. The confirmation page at /registration/[qr_code] displays the QR code for check-in.

5

Build the organizer dashboard with check-in scanner

Create the organizer management page with attendee list, check-in functionality using the browser camera for QR scanning, and event sales analytics.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build an organizer dashboard at app/events/[id]/manage/page.tsx.
3// Requirements:
4// - Server Component that fetches event with all registrations and ticket tier stats
5// - Show event summary Card: title, date, total registrations, total revenue, check-in rate
6// - Attendee Table with columns: name, email, ticket tier Badge, registration date, checked-in status Badge (green checkmark or gray pending)
7// - A "Check In" Button on each row that calls a Server Action to mark checked_in = true and set checked_in_at
8// - A "Scan QR" Button that opens a Dialog with the browser camera (using getUserMedia API) to scan QR codes
9// - The scan route at app/api/checkin/route.ts validates the qr_code, marks the registration as checked in, and returns attendee name
10// - Show ticket tier breakdown: Card for each tier with name, sold/total count, revenue, and Progress bar
11// - Add a "Download CSV" Button that exports the attendee list
12// - Use Tabs to switch between Attendees, Analytics, and Settings views

Pro tip: Use the Vars tab to store a QR_SECRET key for HMAC-signing QR codes, preventing forgery. Validate the signature in the check-in API route.

Expected result: Organizers see all attendees, can check them in manually or via QR scan, and view sales analytics broken down by ticket tier.

Complete code

app/api/register/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import Stripe from 'stripe'
3import { createClient } from '@supabase/supabase-js'
4import { randomBytes } from 'crypto'
5
6const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
7const supabase = createClient(
8 process.env.SUPABASE_URL!,
9 process.env.SUPABASE_SERVICE_ROLE_KEY!
10)
11
12export async function POST(req: NextRequest) {
13 const { eventId, tierId, attendeeName, attendeeEmail } = await req.json()
14
15 const { data: reserved } = await supabase.rpc('reserve_ticket', {
16 tier_id: tierId,
17 })
18
19 if (!reserved) {
20 return NextResponse.json(
21 { error: 'Tickets sold out for this tier' },
22 { status: 409 }
23 )
24 }
25
26 const { data: tier } = await supabase
27 .from('ticket_tiers')
28 .select('name, price_cents')
29 .eq('id', tierId)
30 .single()
31
32 const qrCode = randomBytes(16).toString('hex')
33
34 const session = await stripe.checkout.sessions.create({
35 payment_method_types: ['card'],
36 customer_email: attendeeEmail,
37 mode: 'payment',
38 line_items: [
39 {
40 price_data: {
41 currency: 'usd',
42 unit_amount: tier!.price_cents,
43 product_data: { name: `${tier!.name} ticket` },
44 },
45 quantity: 1,
46 },
47 ],
48 metadata: { eventId, tierId, attendeeName, attendeeEmail, qrCode },
49 success_url: `${req.nextUrl.origin}/registration/${qrCode}`,
50 cancel_url: `${req.nextUrl.origin}/events/${eventId}`,
51 })
52
53 return NextResponse.json({ url: session.url })
54}

Customization ideas

Add promo codes for discounts

Create a promo_codes table and add a code input to the registration form. Apply percentage or fixed-amount discounts at checkout by adjusting the Stripe line item price.

Add email confirmations with calendar invite

Send a confirmation email via Resend after registration that includes an .ics calendar file attachment for easy calendar import.

Add multi-event passes

Create a pass system where attendees can purchase access to multiple events at a discounted bundle price with a single checkout.

Add live attendance analytics

Use Supabase Realtime to show a live check-in counter on the organizer dashboard, updating in real-time as attendees scan their QR codes.

Common pitfalls

Pitfall: Using a regular SELECT + UPDATE instead of atomic reservation for ticket counts

How to avoid: Use a PostgreSQL function that atomically checks AND increments in a single UPDATE WHERE clause: SET sold_count = sold_count + 1 WHERE sold_count < quantity.

Pitfall: Using request.json() in the Stripe webhook handler

How to avoid: Use await req.text() to get the raw body, then pass it to stripe.webhooks.constructEvent().

Pitfall: Generating QR codes on the client side without server validation

How to avoid: Generate QR codes server-side with cryptographic randomness and store them in the database. The check-in route validates the code exists and hasn't been used.

Best practices

  • Use atomic PostgreSQL functions (RPC) for ticket reservation to prevent overselling under concurrent registrations.
  • Generate QR codes server-side with cryptographic randomness (randomBytes) and store them in the database for validation.
  • Use request.text() in the Stripe webhook handler for proper signature verification.
  • Store ticket prices in cents as integers and display with (price_cents / 100).toFixed(2) to avoid floating-point issues.
  • Use Server Components for event listing and detail pages for SEO and fast initial loads.
  • Set STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, and QR_SECRET in the Vars tab without NEXT_PUBLIC_ prefix.

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building an event registration system with Next.js and Supabase. I need to prevent overselling tickets when multiple users register simultaneously. Show me how to create a PostgreSQL function that atomically reserves a ticket by incrementing sold_count only if it's less than quantity, and how to call it from a Next.js API route via Supabase RPC.

Build Prompt

Build the QR code check-in system for an event registration app. Create app/api/checkin/route.ts that accepts a POST with qr_code, validates it against the registrations table, marks checked_in = true with checked_in_at timestamp, and returns the attendee name. Add error handling for invalid codes and already-checked-in attendees. Build a 'use client' scanner component using getUserMedia for the browser camera.

Frequently asked questions

How do I prevent overselling tickets?

Use a PostgreSQL function (reserve_ticket) that atomically checks sold_count < quantity and increments in a single UPDATE statement. If the tier is sold out, no row is returned and the API returns a 409 Conflict error.

How does QR code check-in work?

Each registration generates a unique QR code stored in the database. The organizer scans it using the browser camera or enters it manually. The check-in API validates the code exists, hasn't been used, and marks it as checked in.

Can I offer free events without Stripe?

Yes. For free ticket tiers (price_cents = 0), skip the Stripe Checkout step and insert the registration directly. The reserve_ticket function still prevents over-capacity.

How do I deploy and set up the webhook?

Publish via V0's Share menu, then register the webhook URL (yourdomain.com/api/webhooks/stripe) in the Stripe Dashboard for checkout.session.completed events. Set STRIPE_WEBHOOK_SECRET in the Vars tab.

Can I add a waitlist for sold-out events?

Yes. When reserve_ticket returns null (sold out), offer to add the user's email to the waitlist table. When a cancellation opens a spot, notify the first waitlisted person via email.

Can RapidDev help build a custom event registration system?

Yes. RapidDev has built 600+ apps including conference management platforms with multi-track schedules, speaker management, and sponsor tiers. Book a free consultation to discuss your event needs.

What V0 plan do I need?

Premium ($20/month) is recommended for the Stripe integration and organizer dashboard. Free tier works for the basic event listing but may need manual coding for the payment flow.

RapidDev

Talk to an Expert

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

Book a free consultation

Need help building your app?

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

Book a free consultation

We put the rapid in RapidDev

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