Quiz.app is a full-stack platform for creating and taking quizzes, built using the MERN stack (MongoDB, Express, React + Vite, Node.js) with Firebase authentication and file storage. We created this application in just 10 days during the Makers Academy remote Software Engineering bootcamp in January 2026, working in a simulated software engineering team using Agile practices.
We worked individually and as a team with morning stand-ups, daily Slack communication, and end-of-day retros. We focused on collaboration, clear communication, and learning good software engineering practices and patterns.
We dealt with real engineering challenges like version control conflicts, debugging, testing, and delivering features under time pressure, while prioritising what mattered most (“keeping the most important things the most important things”).
The project is deployed on Reder under www.quizr.fun
- Welcome to our Drawing Board with diagrams, retro and MVP 🎨 🖌️
- Dive into our Team Charter & Process 📜✨
Current core features include:
- Authentication: Firebase sign up / log in / log out
- Authorisation: protected routes + auth-aware navbar redirects
- Navigation: global navbar across the app
- Home: landing layout with a Create Quiz entry point
- Create Quiz: create a quiz title, add questions, add answer options, set the correct answer, save using the latest quiz schema
- Quiz Discovery: view public quizzes on user profile pages (Quizzes section)
- Take Quiz: dynamic question pages, back/next navigation, answers saved during the flow, submit attempt as one payload
- Scoring: server-side scoring + attempt saved per user
- Profiles: username display, own-profile edit button, quizzes taken, favourites/saved quizzes (where enabled)
- Leaderboards: per-quiz Top N leaderboard + your best score
- Friends: friend requests with status (pending/accepted) + friendship metadata (requestedBy, createdAt, acceptedAt)
- Friends Access: view quizzes within your friend network (where enabled)
- Seed Data: natural seed users/quizzes + Firebase user seeding (where required)
- UI: responsive Tailwind CSS styling across core pages (Home, Auth, Profile, Quizzes)
- Profile linking: link to user profiles from Friends and Quizzes views
- Leaderboards: friends-only leaderboard view
- Deployment: deploy the database
- Multi-answer questions: allow multiple correct answers + allow users to select more than one answer
- Difficulty insights: difficulty ratings + average completion rate
- Profile stats: show common quiz topics + average difficulty for created quizzes
- Media: Firebase image upload + user profile picture
- Quiz generation: LLM-assisted quiz creation (auto-generate quizzes)
- Mistake follow-up: missed questions show a short explanation and/or a mini-quiz later
- Adaptive difficulty: questions adjust based on performance
- Randomisation: randomise question order during quiz creation/delivery
- Streaks: quiz completion streaks
This repo contains two applications:
api/– Node + Express backend (secured with Firebase Admin)frontend/– React + Vite SPA, styled with Tailwind CSS
They communicate via HTTP:
- The frontend reads the API base URL from
VITE_BACKEND_URL - All API requests go through a custom
apiFetch(path, options)helper apiFetchattaches the current Firebase ID token asAuthorization: Bearer <token>- The backend verifies this token with Firebase Admin and uses
req.userfor identity - Application data is stored in MongoDB (via Mongoose)
Teamwork & delivery flow:
- We communicated clearly about tasks, progress and blockers ✅
- Took shared ownership and stayed focused on shipping a complete, working product ✅
- Supported each other from start to finish ✅
Minimum Viable Product delivered on time:
- We shipped a working MVP within the project timeline ✅
- With the core features implemented and stable ✅
- Stable enough for real users to use end-to-end. ✅
Functionality & UI leveled up:
- We iterated on both functionality and design ✅
- Refining layouts, improving usability ✅
- Making the interface more consistent and user-friendly over time✅
Bugs & debugging - we improved by:
- Discussing issues as a team before making changes ✅
- Logging bugs in Trello board to make sure nothing was missed ✅
- Screen sharing on Zoom and testing before merging ✅
Git & merge conflicts - we improved by:
- Creating smaller pull requests ✅
- Merging to main more often ✅
- Communicating before touching the same files ✅
Time management - we improved by:
- Balancing learning new tech vs. actually shipping features ✅
- Had to drop/scale back some ideas to hit the deadline ✅
- Taking regular breaks for gym, run, walk in the park etc ✅
- Set-up instructions were provided as a starting template for us to use
- We created our own seed project (the base version of the app) and extended it throughout development
Our backend is locked by default. Firebase Auth proves who the user is (email, uid), and Express verifies that token once globally. If you’re not authenticated, nothing gets through.
- Never call
fetch()directly for our API - Always use:
apiFetch(path, options)fromsrc/services/api.js - Auth is handled automatically by
apiFetch
Import Firebase directly only on auth pages (done) and file storage features For everything else, use this:
import { apiFetch } from "../services/api";And then:
const res = await apiFetch("/quizzes");Example src/services/quizzes.js file:
import { apiFetch } from "./api";
export async function getQuizzes() {
const res = await apiFetch("/quizzes");
if (!res.ok) throw new Error("Failed to load quizzes");
return res.json();
}
export async function createQuiz(data) {
const res = await apiFetch("/quizzes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
return res.json();
}No auth tokens passed, all of that is already handles by the api helper.
- Auth is global by default, because
app.use(requireAuth)is inapi/app.js - In controllers, the user identity is always
req.user.uid - Never accept
userIdfrom the client
Example:
function createQuiz(req, res) {
const ownerUid = req.user.uid;
}Never do:
req.body.userId
req.params.userIdThe user identity always comes from Firebase, never the client.
In api/app.js, for example /quizzes route:
app.use("/quizzes", quizzesRouter);No auth logic here. It’s already enforced globally.
src/services/firebase.js- Firebase client init (auth + storage)src/services/authentication.js- login/signup (Firebase Auth only)src/services/api.js- centralized authenticated fetchpages/Login/*- login UI + redirect logicpages/Signup/*- signup UI + redirect logicpages/Home/*- auth-guarded page (example)
api/middleware/requireAuth.js- verifies Firebase ID tokenapi/lib/firebaseAdmin.js- Firebase Admin initapi/app.js- global auth gateapi/routes/*- normal routers (no auth logic inside)api/controllers/*- always usereq.user.uid
Internally, apiFetch retrieves the Firebase ID token from the current session and sends it as an Authorization: Bearer <token> header. The backend verifies this token once per request.
