Next.js 16 (App Router) + TypeScript + Tailwind CSS v4 + Firebase client SDK (Auth, Firestore, Storage), with optional EmailJS confirmation emails. Organizers create free events and take registrations (one per email address); attendees register without an account and are checked in at the door with a unique ticket code.
- Create a Firebase project, then add a Web app. Enable Authentication → Email/Password, Firestore and Storage.
- Copy
.env.exampleto.env.localand paste your web app config. The EmailJS keys are optional. - Deploy the security rules and indexes in this repo:
The storage rule checks event ownership in Firestore. Accept the cross-service permission prompt when you deploy it.
npm install -g firebase-tools firebase login firebase use --add # pick your project firebase deploy --only firestore:rules,firestore:indexes,storage - Run the app:
Open http://localhost:3000, create an account at
npm install npm run dev
/auth/register, then create your first event.
| Route | What it does | Access |
|---|---|---|
/ |
Marketing homepage (hero, features, how it works, ticket lookup, FAQ + CTA) | Public |
/auth/login |
Email + password sign in | Public |
/auth/register |
Username, email, password sign up | Public |
/auth/forgot-password |
Email a password-reset link | Public |
/dashboard |
Stats, events (filter + search), recent registrations | Organizer |
/events/create |
Create event: basics, date, location, capacity, form fields | Organizer |
/events/[id]/edit |
Edit an event (registrations are kept) | Event owner only |
/events/[id] |
Manage event: attendees, check-in status, CSV export, close/reopen registration | Event owner only |
/events/[id]/register |
Attendee registration form (no account needed) | Public |
/events/[id]/register/success |
Confirmation + unique ticket code | Public |
/events/[id]/search |
Venue check-in: search by name or ticket code | Event owner only |
lib/
firebase.ts ← Firebase app/auth/firestore/storage setup (reads .env.local)
firebase-actions.ts ← EVERY Firebase call lives here (auth, events, attendees, check-in, email)
utils.ts images.ts
app/ ← routes (pages only, no server code)
components/
ui/ ← reusable primitives (Button, Field, Badge, StatCard, TicketCard…)
layout/ ← headers, footer, AppShell, RequireAuth
home/ auth/ dashboard/ events/ register/ checkin/ ← page sections
providers/ ← AuthProvider (Firebase Auth state), ToastProvider
hooks/use-subscription.ts ← subscribes components to live Firestore data
images/ ← logo + preset event cover artwork
styles/globals.css ← Tailwind theme tokens + shared utilities
types/ ← shared TypeScript types (and the Firestore layout)
firestore.rules storage.rules firestore.indexes.json firebase.json
Components never import the Firebase SDK directly. They call functions from lib/firebase-actions.ts:
// one-off actions return a Promise and throw a readable Error
await createEvent(user.id, input, coverDataUrl);
await registerAttendee(eventId, { name, email, tickets: 1 });
// live data: subscribe + unsubscribe, wrapped by the useSubscription hook
const { data: event, loading } = useSubscription(subscribeToEvent, [eventId]);Google Maps (venue maps): in Google Cloud, enable the Maps Embed API, create an API key
restricted to your site's domains, and set NEXT_PUBLIC_GOOGLE_MAPS_API_KEY. The Embed API is free.
App Check (spam protection):
- Create a reCAPTCHA v3 key at https://www.google.com/recaptcha/admin and add your domains (and
localhost). - Firebase console → App Check → register your web app with that site key, and paste the reCAPTCHA secret.
- Set
NEXT_PUBLIC_RECAPTCHA_SITE_KEYand restart the dev server. - On localhost the browser console prints an App Check debug token. Add it under App Check → Apps → ⋮ → Manage debug tokens.
- Once requests show as "verified" in App Check's metrics, click Enforce for Firestore and Storage.
The registration form also has a hidden honeypot field and a minimum fill time, which stop simple bots.
Event times are stored as Firestore Timestamps, which are exact moments in time. Everyone sees them in their own time zone with a label (e.g. 6:00 PM GMT+1). If a viewer is in a different zone from the event, the event's local time is shown as well. Registration closes at the deadline, or when the event ends. The database rules enforce that with the server's clock.
| Path | Contents | Who can read |
|---|---|---|
users/{uid} |
username, email | That user |
events/{eventId} |
event details, ownerId, registeredCount, checkedInCount |
Everyone |
events/{eventId}/attendees/{ticketCode} |
name, ticket code, check-in status | One ticket by its code: anyone. Full list + check-in: event owner |
events/{eventId}/contacts/{ticketCode} |
email, phone, company | Event owner only |
events/{eventId}/registrations/{emailKey} |
one entry per email (SHA-256 of eventId:email, no personal data) so an email can only register once per event |
Anyone can check one; list: event owner |
Contact details are kept in a separate subcollection because Firestore rules can't hide individual fields, so a ticket can be looked up by its code without exposing the attendee's email.
Registration runs in a transaction, so an event can't be overfilled and an email can't register twice.
- Check-in access: only the event creator can open
/events/[id]/searchand check guests in (enforced byfirestore.rules). To let others help at the door, add a staff role or PIN. - Emails: set the EmailJS env vars. Your template can use
name,email,title,when,location,description,ticket_codeandregister_url.
Every page is built mobile-first with Tailwind breakpoints (sm 640, md 768, lg 1024, xl 1280).
Navigation collapses into a menu button. Two-column layouts stack on small screens. The attendee table
switches to cards on phones. All tap targets are at least 44px.