After excessive procrastination, I decided to improve my developer portfolio by building real-world projects that stand out from the job-hunting pool. Then, I came up with an idea, an event ticketing system.
In this tutorial, I'll walk you through how I built an event ticketing system with Next.js and Firebase.
Upon completion, you'll be able to create advanced web applications using Firebase's amazing features, like real-time database, authentication, and file storage.
💡 PS: This tutorial assumes you have a basic knowledge of React or Next.js.
Application Workflow
Before we start coding, let me summarise how the application works. The application does the following:
- authenticates users via Email and Password with Firebase,
- allows users to create events and generate a registration link to share with friends or the public.
- when someone registers via your invite link, they receive the event ticket in their email, including a passcode which you can use to verify their event ticket from your dashboard.
- The application also allows you as a user to view the number of people who registered for your event, validate an attendee's ticket via the passcode they receive, and also disable the registration link when you have enough attendees. You can also delete an event after it has occurred.
💡 Check out the live version of the application.
The Design process
Here, I'll walk you through creating the required pages for the web application.
First of all, you need a homepage for the application. The home page should have a link where new users can create an account and another for existing users to log into their accounts.
Next, create the sign-in and sign-up page. In this article, I'll use the Email and Password authentication method.
After successful login, users can create new events, view existing events, and log out of the application on a single page, called the dashboard page.
Next, you need to allow users to view the attendees for each event, disable registration, validate the user's ticket at the venue, and delete an event.
Therefore, you need to create a route for each event. You may adapt my method, where I made each event clickable redirecting users to another page containing every detail of the particular event.
From the image above, users can disable the registration link for the event and view and validate the attendees' list.
Finally, create the event registration page. Before showing this page to a visitor, you need to check if the event registration link has not been disabled.
After validation, users should supply their name and email, be added to the attendees' list, and receive an email containing the event details.
Since you've learnt how to build the pages of the application. Let's code.💪🏾
In the upcoming sections, you'll learn how to use the various features provided by Firebase.
What is Firebase?
Firebase is a Backend-as-a-Service (Baas) owned by Google that enables developers to build full-stack web applications in a few minutes. Services like Firebase make it very easy for front-end developers to build full-stack web applications with little or no backend programming skills.
Firebase provides various authentication methods, a NoSQL database, a real-time database, file storage, cloud functions, hosting services, and many more.
How to add Firebase to a Next.js application
To add Firebase to a Next.js app, follow the steps below:
Visit the Firebase console and sign in with a Gmail account.
Create a Firebase project once you are signed in.
Select the </>
icon to create a new Firebase web app.
Provide the name of your app and register the app.
Install the Firebase SDK by running the code snippet below.
npm install firebase
Create a firebase.js
file at the root of your Next.js project and copy the Firebase configuration code for your app into the file.
import { initializeApp } from "firebase/app";
import { getAnalytics } from "firebase/analytics";
const firebaseConfig = {
apiKey: "******",
authDomain: "**********",
projectId: "********",
storageBucket: "******",
messagingSenderId: "**********",
appId: "********",
measurementId: "********",
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);
Finally, update the firebase.js
to contain some required modules for Firebase Authentication, Storage, and Database.
import { initializeApp, getApps } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { EmailAuthProvider } from "firebase/auth";
import { getAuth } from "firebase/auth";
import { getStorage } from "firebase/storage";
const firebaseConfig = {...<your_config>...};
// 👇🏻 Initializing Firebase in Next.js
let app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
//👇🏻 Initializing the Email/Password Auth
const provider = new EmailAuthProvider();
//👇🏻 Firebase File Storage
const storage = getStorage(app);
//👇🏻 Firebase Data Storage
const db = getFirestore(app);
//👇🏻 Firebase Auth
const auth = getAuth(app);
export { provider, auth, storage };
export default db;
Congratulations!🎉 You've successfully added Firebase to your Next.js app. Next, let's set up the needed Firebase features.
Setting up Firebase Authentication
Before you can add Firebase Authentication to your application, you need to set it up on your console.
Select Build on the left-hand panel, and click Authentication.
Click the Get Started
button, enable the Email/Password method, and click Save.
If successful, your screen should display this:
Setting up Firebase Firestore
Select Firestore Database
from the left-hand side menu and create a database.
Create the database in test mode, and use the default Cloud Firestore location settings.
After creating your database, select Usage
from the top menu bar, edit the rules, and publish the changes. This enables you to make requests to the database for a longer period of time.
Congratulations!🎉 Your Firebase Database is ready.
Setting up Firebase Storage
Select Storage
from the left-hand side menu, and create the storage in test mode using the default location settings.
Update the timestamp for the Firebase Storage as done in the Firebase Firestore above.
Finally, you've set up your Firebase console. Next, I will walk you through how to communicate with Firebase by building the event ticketing system.
Communicating with Firebase: Authenticating users
In this section, I'll walk you through the authentication aspect of the event ticketing system.
You can create a utils
folder containing the functions and import them into the required components.
Signing up new users
This function is executed on the SignUp page. It accepts the user's email and password and creates an account with the credentials.
import { createUserWithEmailAndPassword } from "firebase/auth";
import { useRouter } from "next/router";
import { auth } from "./firebase";
const router = useRouter();
export const firebaseCreateUser = (email, password, router) => {
createUserWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
const user = userCredential.user;
successMessage("Account created 🎉");
router.push("/login");
})
.catch((error) => {
console.error(error);
errorMessage("Account creation declined ❌");
});
};
The code snippet above accepts the useRouter
hook and the user's email and password then creates an account for the user.
With the createUserWithEmailAndPassword
function, Firebase handles the authentication process. If successful, the user is notified and redirected to the login page; otherwise displays an error message.
Signing in existing users
This function allows existing users to access the application. It accepts the user's email and password and returns a user
object containing all the user's information.
import { signInWithEmailAndPassword } from "firebase/auth";
import { useRouter } from "next/router";
import { auth } from "./firebase";
const router = useRouter();
export const firebaseLoginUser = (email, password, router) => {
signInWithEmailAndPassword(auth, email, password)
.then((userCredential) => {
const user = userCredential.user;
successMessage("Authentication successful 🎉");
router.push("/dashboard");
})
.catch((error) => {
console.error(error);
errorMessage("Incorrect Email/Password ❌");
});
};
The code snippet above validates the user's credentials and returns an object containing all the information related to the user. If the process is successful, it redirects the user to the dashboard page; otherwise returns an error.
Logging users out
Firebase also provides a signOut
function that enables users to log out of the application.
Here is how it works:
import { signOut } from "firebase/auth";
import { useRouter } from "next/router";
import { auth } from "./firebase";
const router = useRouter();
export const firebaseLogOut = (router) => {
signOut(auth)
.then(() => {
successMessage("Logout successful! 🎉");
router.push("/");
})
.catch((error) => {
errorMessage("Couldn't sign out ❌");
});
};
The code snippet above logs users out of the application by getting the active user's details and logging them out with the help of the signOut
function.
Protecting pages from unauthenticated users
To do this, you can store the user's information object to a state after logging in or use the Firebase onAuthStateChanged
hook.
Using the onAuthStateChanged
hook:
import { onAuthStateChanged } from "firebase/auth";
import React, { useEffect, useCallback } from "react";
import { useRouter } from "next/router";
import { auth } from "./firebase";
const router = useRouter();
const isUserLoggedIn = useCallback(() => {
onAuthStateChanged(auth, (user) => {
if (user) {
setUser({ email: user.email, uid: user.uid });
//👉🏻 Perform an authenticated request
} else {
return router.push("/register");
}
});
}, []);
useEffect(() => {
isUserLoggedIn();
}, [isUserLoggedIn]);
The onAuthStateChanged
hook checks if the user is active and returns the object containing all the user's details. You can execute the function on page load for routes containing protected data.
Communicating with Firebase: Interacting with the database and storage
In this section, you'll learn how to interact with the Firestore Database by creating, updating, and deleting events and uploading images to the Firestore Storage.
Creating new events
After authenticating the users, they should be able to create new events via a link on the dashboard page. Create a form field that accepts similar details, as shown below.
export const addEventToFirebase = async (
id,
title,
date,
time,
venue,
description,
note,
flier,
router
) => {
console.log({
id,
title,
date,
time,
venue,
description,
note,
flier,
});
};
The function above accepts all the data related to each event. The id
attribute in the code snippet above refers to the user's id, not the event's id. Adding the user's id to each event data enables you to query the events on the database via the id.
Since the form field in the image above accepts the event's flier via image upload. Therefore, you need to save the image to Firebase and attach it to the event on the database.
How do you do this? Let's see 💪🏾
Create a function handleFileReader
within the Create Event
component, as done below.
const handleFileReader = (e) => {
const reader = new FileReader();
if (e.target.files[0]) {
reader.readAsDataURL(e.target.files[0]);
}
reader.onload = (readerEvent) => {
setFlier(readerEvent.target.result);
};
};
The handleFileReader
function takes an event parameter, uses the FileReader
object to read the image in base64 string format then saves it into the setFlier
state.
Execute the handleFileReader
function when the uploaded file changes.
<input
name='flier'
type='file'
className='border-[1px] py-2 px-4 rounded-md mb-3'
accept='image/*'
onChange={handleFileReader}
/>
Since you've been able to convert the file to a base64 string, next, upload the event's data to Firebase when a user clicks the Create Event
button.
import db, { storage } from "./firebase";
import { addDoc, collection, doc, updateDoc } from "@firebase/firestore";
import { getDownloadURL, ref, uploadString } from "@firebase/storage";
export const addEventToFirebase = async (
id,
title,
date,
time,
venue,
description,
note,
flier,
router
) => {
const docRef = await addDoc(collection(db, "events"), {
user_id: id,
title,
date,
time,
venue,
description,
note,
slug: createSlug(title),
attendees: [],
disableRegistration: false,
});
const imageRef = ref(storage, `events/${docRef.id}/image`);
if (flier !== null) {
//👇🏻 User uploaded a file
} else {
//👇🏻 No flier uploaded
}
};
- From the code snippet above,
- The
addDoc
function adds the event data to a newly-created Firebase collection called events. -
docRef
is the event document on Firebase containing the object data. - I added three more properties - (
slug, attendees, disableRegistration
) to the event document. - The
attendees
array will contain the list of people who registered for the event. - The
slug
enables us to generate a human-readable URL for each event. - The
disableRegistration
attribute enables us to differentiate events accepting registration from others. - The
imageRef
variable creates a reference between the uploaded flier and the event document. It establishes a one-one relationship between the image and the event document. - Lastly, since users can either choose to upload an event flier or not, you need to ensure that both cases are successful.
- The
Before we proceed, create the createSlug
function. It accepts the event title and creates a human-readable URL string format.
export const createSlug = (sentence) => {
let slug = sentence.toLowerCase().trim();
slug = slug.replace(/[^a-z0-9]+/g, "-");
slug = slug.replace(/^-+|-+$/g, "");
return slug;
};
Finally, update the conditional statement within the addEventToFirebase
function.
//👇🏻 Database reference to the image
const imageRef = ref(storage, `events/${docRef.id}/image`);
if (flier !== null) {
await uploadString(imageRef, flier, "data_url").then(async () => {
//👇🏻 Gets the image URL
const downloadURL = await getDownloadURL(imageRef);
//👇🏻 Updates the docRef, by adding the flier URL to the document
await updateDoc(doc(db, "events", docRef.id), {
flier_url: downloadURL,
});
//Alerts the user that the process was successful
successMessage("Event created! 🎉");
router.push("/dashboard");
});
} else {
successMessage("Event created! 🎉");
router.push("/dashboard");
}
The conditional statement checks if the user uploaded an image. If so, it uploads the image to Firebase Storage and updates the event document with a new property called flier_url
.
The flier_url
property contains the image URL hosted on Firebase Storage. If there is no image, none of this process is required, and the user gets notified that the event has been created.
Getting all the events created by a user
On the Dashboard page, you'll need to display all the events created by the current user.
To do this, query the events
collection on Firebase and return only events whose user_id
attribute matches the current user's ID.
import { collection, doc, onSnapshot, query, where } from "@firebase/firestore";
import db from "./firebase";
export const getEvents = (id, setEvents) => {
try {
const q = query(collection(db, "events"), where("user_id", "==", id));
const unsubscribe = onSnapshot(q, (querySnapshot) => {
const firebaseEvents = [];
querySnapshot.forEach((doc) => {
firebaseEvents.push({ data: doc.data(), id: doc.id });
});
setEvents(firebaseEvents);
return () => unsubscribe();
});
} catch (error) {
console.error(error);
}
};
The code snippet above accepts the user's id, filters the events via the id, and returns only events whose user_id
attribute matches the current user.
Deleting events
Allowing users to delete an event is one of the application's features. The function below shows how you can achieve that:
import { doc, deleteDoc } from "@firebase/firestore";
import { ref, deleteObject } from "@firebase/storage";
import db, { storage } from "./firebase";
export const deleteEvent = async (id) => {
await deleteDoc(doc(db, "events", id));
const imageRef = ref(storage, `events/${id}/image`);
deleteObject(imageRef)
.then(() => {
console.log("Deleted successfully");
})
.catch((error) => {
console.error("Image does not exist");
});
};
The deleteDoc
function deletes an event via its ID, and the deleteObject
deletes the flier attached to the event using the imageRef
.
Disabling new registrations
The application is incomplete if the user who created an event cannot disable registration for the event when the registration deadline has passed or if they have enough guests (attendees).
import { doc, updateDoc } from "@firebase/firestore";
import db from "./firebase";
export const updateRegLink = async (id) => {
const number = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
const eventRef = doc(db, "events", id);
updateDoc(eventRef, {
disableRegistration: true,
});
};
The code snippet above accepts the event's id, generates a random number, and updates the event document by setting the disableRegistration
property to true
.
When users receive the invite to an event with its disableRegistration
property set to true
, the page does not display a registration form; instead, it shows that registration for that event is closed.
Handling attendees registration
Before you add an attendee to an event, you have to do three things:
- Fetch the event document via its ID attached to the invite link.
- Validate the attendee's email with the existing array of attendees at the event.
- If the email does not exist on the list, add the user to the list. Otherwise, return an error showing that the user has already registered for the event.
import { getDoc, doc, updateDoc, arrayUnion } from "@firebase/firestore";
//👇🏻 generates a unique passcode for the attendee
export const generatePasscode = () =>
Math.random().toString(36).substring(2, 10);
export const registerAttendee = async (name, email, event_id) => {
const passcode = generatePasscode();
const eventRef = doc(db, "events", event_id);
const eventSnap = await getDoc(eventRef);
let firebaseEvent = {};
if (eventSnap.exists()) {
firebaseEvent = eventSnap.data();
//👇🏻 gets the attendees' list
const attendees = firebaseEvent.attendees;
//👇🏻 filter the list
const result = attendees.filter((item) => item.email === email);
//👇🏻 if registration is valid
if (result.length === 0 && firebaseEvent.disableRegistration === false) {
//👇🏻 adds the attendee to the list
await updateDoc(eventRef, {
attendees: arrayUnion({
name,
email,
passcode,
}),
});
// 👉🏻 sendEventTicketViaEmail()
successMessage("User registered successfully! ✅");
} else {
errorMessage("User already registered ❌");
}
}
};
💡 The
arrayUnion
function provided by Firebase enables us to push an item to an array attribute on a document.
The code snippet above fetches an event via its ID, gets its attendees' list, and checks whether the user hasn't registered for the event before adding the user to the list of attendees.
The generatePasscode
function generates a random sequence of unique numbers assigned to every user for validating each user's ticket on the event creator's dashboard.
On the event details page, you can show the attendees' list for the event and an input field that searches for attendees via their passcode.
Creating and sending event tickets to attendees
After an attendee has successfully registered for an event, you need to send an email containing the event details and their passcode to the event.
To do this, you can use a JavaScript library that supports email notifications, such as EmailJS, Novu, and Sendgrid.
How to send emails via EmailJS
Here, I'll guide you through how you can add EmailJS to the application.
Install EmailJS to the Next.js application by running the code below:
npm install @emailjs/browser
Create an EmailJS account and add an email service provider to your account.
Add an email template as done in the image below. The words in curly brackets represent variables that can hold dynamic data.
Dear {{name}},
We hope this message finds you bursting with excitement because you are about to embark on a journey like no other! We are thrilled to present your personal ticket details for the most incredible event of the year!
Event Name: {{title}}
Event Description: {{description}}
Time: {{time}}
Date: {{date}} (Save the date)
PS: {{note}}
Please keep your invitation code secret - {{passcode}}. This shows you're part of the attendees.
DOWNLOAD EVENT FLIER - {{flier_url}}
Congratulations! You can send the event tickets using the email template above. Feel free to beautify yours.🔥
Sending the event tickets to attendees with EmailJS
To send the event ticket to the newly registered attendee, update the if
block within the registerAttendee
function, as done below.
if (result.length === 0) {
await updateDoc(eventRef, {
attendees: arrayUnion({
name,
email,
passcode,
}),
});
const flierURL = firebaseEvent.flier_url
? firebaseEvent.flier_url
: "No flier for this event";
sendEmail(
name,
email,
firebaseEvent.title,
firebaseEvent.time,
firebaseEvent.date,
firebaseEvent.note,
firebaseEvent.description,
passcode,
flierURL,
setSuccess,
setLoading
);
} else {
setLoading(false);
errorMessage("User already registered ❌");
}
The code snippet checks if there is no existing user with the same email before adding the new user to the attendees' list. If there is no flier attached to the event, it takes note of that and passes all the event's information as parameters to the sendEmail
function.
Create the sendEmail
function to send the event details to the newly-registered attendee's email.
import emailjs from "@emailjs/browser";
const sendEmail = (
name,
email,
title,
time,
date,
note,
description,
passcode,
flier_url,
setSuccess,
setLoading
) => {
emailjs
.send(
process.env.NEXT_PUBLIC_SERVICE_ID,
process.env.NEXT_PUBLIC_TEMPLATE_ID,
{
name,
email,
title,
time,
date,
note,
description,
passcode,
flier_url,
},
process.env.NEXT_PUBLIC_API_KEY
)
.then(
(result) => {
setSuccess(true);
//👉🏻 Email sent ✅
},
(error) => {
alert(error.text);
}
);
};
The sendEmail
function accepts all the required parameters and sends the email using the EmailJS library.
Conclusion
Congratulations on making it thus far! You've learnt
- what Firebase is,
- how to add Firebase to a Next.js app,
- how to work with Firebase Auth, Storage, and Database, and
- how to build an event ticketing system.
I've built a live version of the application, check it out - https://eventtiz.vercel.app. The source code is also available here
Firebase is a great tool that provides almost everything you need to build a full-stack web application. If you want to create a full-stack web application without any backend programming experience, consider using Firebase.
Thank you for reading! 🎉
Open to work🙂
Did you enjoy this article or need an experienced Technical Writer / React Developer for a remote, full-time, or contract-based role? Feel free to contact me.
GitHub || LinkedIn || Twitter
Top comments (21)
Incredibly detailed post! Kuddos on this! Another alternative to email or to include in email is a ticket created using Vercel’s Image Generation.
Also a chef’s kiss would’ve been to frame the screenshots included in this post. I’ve created a simple OSS tool for this. Check it out and let me know what you think.
github.com/ekqt/screenshot
Sure, thank you, Hector!
Great and detail explanation ✨
Thank you, Ron 🔥
Just what I needed. Saved for tonight’s session.
Awesome, 💪
Nice Article and well written! Thanks for sharing
Glad, you enjoyed reading it Femi!
Great article and interesting idea for project!
Thank you, Oleg! 🙂
Awesome article!
Thank you, Avi! 😍
great article
Thank you, Dami 💪
Thank you, Abdullah
Thank you for this detailed post. For me, as a mostly BE developer it was impressive to see what is possible to do with firebase. For me as a person also interested a bit in security I had to give it a spin and see if I can manipulate others events. Took some searching, but it does work.
By changing a query in the browsers dev tool
I was able to see all events (excluding my own in this case)
I was also able to delete a event created by a second user I've registered.
Any idea how to prevent such attacks in your app?
Wow, this is new to me. I don't have any idea about this at the moment.
But, I will make some research, maybe it's from my end or Firebase.
Thank you for pointing it out, Martin.🫡
I'm curious what you find.
Maybe explaining how this came to my mind helps in figuring out how to prevent such attacks.
Seeing the listing to get all events by a user from firebase triggered me to try and modify the query. I was aiming for changing the network request directly but could not figure this out. Then I took a look at the source code in the developer tools and after some searching and debugging I found the correct place to edit the query in code while debugging.
Have a look here 😉
THANK YOU SO MUCH :)