Firebase Guide
Learn how we use Firebase for authentication, database, and storage
Firebase is Google's platform for building web and mobile applications. We use Firebase for authentication, database storage (Firestore), and file storage.
This guide will teach you exactly how we use Firebase in this project, even if you've never used Firebase before.
What is Firebase?
Firebase is a Backend-as-a-Service (BaaS) that provides ready-to-use backend features:
- Authentication: User login/signup with email/password or Google
- Firestore: NoSQL database to store data (users, teams, events, positions)
- Storage: File storage for images, PDFs, resumes (like a CDN/bucket)
- Cloud Functions: Serverless functions that run in the cloud
Best part? Firebase has a generous free tier. We optimize our usage to stay within these limits.
๐ Official Documentation:
Firebase Documentation1. Firebase Authentication
How Authentication Works
Firebase Auth handles user login and signup. We support two methods:
- Email/Password: Traditional username and password
- Google OAuth: "Sign in with Google" button
Authentication flow in our project:
- User logs in via the login form (
src/components/login-form.tsx) - Firebase authenticates them and returns a user object
- We store the auth state globally using React Context (
src/contexts/AuthContext.tsx) - Protected pages (like
/admin) check if the user is logged in - A service worker automatically adds the user's authentication token to requests
๐ Official Documentation:
Firebase AuthenticationService Worker for Authentication
We use a service worker to handle authentication tokens automatically. This is important for Server-Side Rendering (SSR) with Firebase.
๐ก What's a Service Worker?
A service worker is a script that runs in the background of your browser. It can intercept network requests.
Think of it like a security guard who automatically adds your ID badge to every request you make.
Why we need this:
When using Next.js Server Components (pages that render on the server), the server needs to know who the user is. The service worker ensures the auth token is sent with every request, so the server can verify the user's identity.
Our service worker is defined in public/worker.template.js.
๐ Official Documentation:
Service Worker Sessions2. Firebase Firestore (Database)
โ ๏ธ MOST IMPORTANT SECTION
This is the most critical part of working with Firebase in our project. Read this carefully!
What is Firestore?
Firestore is a NoSQL database that stores data in "collections" and "documents".
Think of it like this:
- Collection: A folder (e.g., "teams", "positions", "events")
- Document: A file inside that folder (e.g., a specific team or position)
Example structure:
teams/ (collection)
โโ team-123/ (document)
โ โโ name: "Marketing Team"
โ โโ description: "Handles social media"
โ โโ members: [...]
โโ team-456/ (document)
โโ name: "Development Team"
โโ ...GOLDEN RULE: NEVER Fetch Directly from Firebase
RULE: You should NEVER import and use Firestore functions (like getDoc, getDocs) directly in a page or component.
INSTEAD: You MUST use our Type Classes located in src/app/types/.
โ WRONG (Don't do this):
import { getDoc, doc } from "firebase/firestore";
import { db } from "@/lib/firebase";
// DON'T DO THIS!
const teamDoc = await getDoc(doc(db, "teams", "team-123"));โ CORRECT (Do this):
import { Team } from "@/app/types/team";
// Always use the Type Class
const team = await Team.read("team-123");Why use Type Classes?
- Consistent structure for data
- Built-in CRUD operations (Create, Read, Update, Delete)
- Type safety with TypeScript
- Automatic timestamp handling
- Support for both client-side and server-side fetching
- Money-saving optimizations built-in
Understanding Type Classes
Every data model (Team, Position, Event, etc.) has a corresponding Type Class in src/app/types/.
Let's break down a Type Class using src/app/types/team/index.ts as an example:
Part 1: Type Definition
This defines the shape of the data (what fields exist and their types):
export type TeamType = {
id: string;
name: string;
description: string;
members: TeamMember[];
createdAt: Timestamp;
updatedAt: Timestamp;
};Part 2: Class Implementation
The class implements methods to Create, Read, Update, and Delete data:
export class Team implements TeamType {
// Properties (same as TeamType)
id: string;
name: string;
description: string;
members: TeamMember[];
createdAt: Timestamp;
updatedAt: Timestamp;
// Constructor - creates a new Team instance
constructor(data: TeamType) { ... }
// Converter - tells Firebase how to save/load data
static converter = { ... };
// CREATE: Add a new team to Firestore
async create(): Promise<string> { ... }
// READ: Get a single team by ID
static async read(
id: string,
options?: { server?: boolean }
): Promise<Team | null> { ... }
// READ ALL: Get all teams
static async readAll(
options?: { server?: boolean, public?: boolean }
): Promise<Team[]> { ... }
// UPDATE: Update an existing team
async update(): Promise<void> { ... }
// DELETE: Delete a team
async delete(): Promise<void> { ... }
}CRUD Operations Explained
CRUD stands for Create, Read, Update, Delete โ the four basic operations for any database.
CREATE: Adding New Data
When you want to add a new team, position, event, etc. to the database:
import { Team } from "@/app/types/team";
import { serverTimestamp, Timestamp } from "firebase/firestore";
// Create a new Team instance
const newTeam = new Team({
id: "", // Will be auto-generated by Firebase
name: "Marketing Team",
description: "Handles social media and outreach",
members: [],
createdAt: serverTimestamp() as Timestamp,
updatedAt: serverTimestamp() as Timestamp,
});
// Save it to Firestore (returns the generated ID)
const teamId = await newTeam.create();
console.log("Team created with ID:", teamId);READ: Getting Data (Single Item)
When you want to get a specific team by its ID:
Client-side (in a React component with 'use client'):
import { Team } from "@/app/types/team";
const team = await Team.read("team-id-123");
if (team) {
console.log(team.name); // "Marketing Team"
}Server-side (in a Server Component or API route):
import { Team } from "@/app/types/team";
const team = await Team.read("team-id-123", { server: true });Public access (no authentication required):
import { Team } from "@/app/types/team";
const team = await Team.read("team-id-123", {
server: true,
public: true
});READ: Getting Data (All Items)
When you want to get all teams:
Client-side:
import { Team } from "@/app/types/team";
const allTeams = await Team.readAll();
console.log(allTeams.length); // e.g., 5 teamsServer-side:
import { Team } from "@/app/types/team";
const allTeams = await Team.readAll({ server: true });Public access (IMPORTANT for SSR):
import { Team } from "@/app/types/team";
const allTeams = await Team.readAll({
server: true,
public: true
});UPDATE: Modifying Existing Data
When you want to change an existing team's data:
import { Team } from "@/app/types/team";
// First, get the team
const team = await Team.read("team-id-123");
if (team) {
// Modify the properties
team.name = "Updated Marketing Team";
team.description = "New description here";
// Save the changes to Firestore
await team.update();
console.log("Team updated!");
}DELETE: Removing Data
When you want to delete a team from the database:
import { Team } from "@/app/types/team";
// First, get the team
const team = await Team.read("team-id-123");
if (team) {
// Delete it from Firestore
await team.delete();
console.log("Team deleted!");
}๐ฐ The `public` Option: Saving Money
Notice the { server: true, public: true } option? This is crucial for understanding how we save money on Firebase.
What's the difference?
- Without
public: true: Fetches require authentication (uses authenticated Firestore instance) - With
public: true: Fetches data without authentication (uses public Firestore instance)
Why does this matter?
Firebase charges based on the number of reads/writes. When we use public: true, we can cache the data more aggressively and serve it to users without checking authentication every time. This dramatically reduces costs.
โ
When to use public: true:
- Data that rarely changes (team members, positions)
- Data that everyone can see (public events, project showcases)
- Pages that need to load quickly without authentication
- Server-Side Rendered pages (SSR)
โ When NOT to use it:
- Admin pages
- User-specific data (applications, profiles)
- Data that changes frequently
- Sensitive information
Client vs Server Firestore
We have two separate Firestore libraries:
1. Client Firestore (src/lib/firebase/client/firestore.ts)
- Runs in the browser
- Used in Client Components (components with
'use client') - Requires Firebase to be initialized on the client
2. Server Firestore (src/lib/firebase/server/firestore.ts)
- Runs on the server (Next.js server or Edge Runtime)
- Used in Server Components and API routes
- Supports
public: trueoption for unauthenticated access - Marked with
"use server"at the top
The Type Classes automatically choose the right library:
// Uses client library (runs in browser)
const teams = await Team.readAll();
// Uses server library (runs on server)
const teams = await Team.readAll({ server: true });
// Uses server library with public access
const teams = await Team.readAll({ server: true, public: true });3. Server-Side Rendering (SSR) with Firebase
What is SSR and Why It Matters
Next.js allows us to render pages on the server before sending them to the user. This is called Server-Side Rendering (SSR).
When combined with Firebase, SSR has huge benefits:
- Faster page loads: Data is already loaded when the page is sent to the user
- Better SEO: Search engines can see the content immediately
- Lower Firebase costs: We can cache data and avoid repeated fetches
How We Use SSR with Firebase
We use SSR for pages that show data that rarely changes, such as:
- Team members page (
/team) - Open positions page (
/positions) - Project showcase page (
/projects) - Public events page (
/events)
By rendering these pages on the server and using the public: true option, we can:
- Fetch data once on the server
- Cache the result
- Serve the same page to all users without re-fetching
This keeps us well within Firebase's free tier!
Example: SSR Page with Firebase
// app/team/page.tsx (Server Component)
import { Team } from "@/app/types/team";
export default async function TeamPage() {
// Fetch teams on the server with public access
const teams = await Team.readAll({
server: true,
public: true
});
return (
<div>
<h1>Our Teams</h1>
{teams.map((team) => (
<div key={team.id}>
<h2>{team.name}</h2>
<p>{team.description}</p>
</div>
))}
</div>
);
}What happens:
- Next.js runs this code on the server
Team.readAlluses the server Firestore library with public access- Data is fetched from Firebase once
- The HTML is generated with the data already in it
- The HTML is sent to the user's browser
- No client-side Firebase fetching needed!
๐ Official Documentation:
4. Firebase Storage (File Uploads)
What is Firebase Storage?
Firebase Storage is like a file bucket or CDN where we store files like:
- Profile pictures
- Event images
- Resumes (for job applications)
- PDFs (documents, resources)
We use the client-side storage library (src/lib/firebase/client/storage.ts) for all storage operations.
Common Storage Operations
Upload a File
import { uploadFile } from "@/lib/firebase/client/storage";
const file = /* get file from input */;
const result = await uploadFile(file, "resumes/user-123.pdf");
console.log("File uploaded:", result.downloadURL);Get File URL
import { getFileURL } from "@/lib/firebase/client/storage";
const url = await getFileURL("resumes/user-123.pdf");
console.log("Download URL:", url);Delete a File
import { deleteFile } from "@/lib/firebase/client/storage";
await deleteFile("resumes/user-123.pdf");List Files in a Directory
import { listFiles } from "@/lib/firebase/client/storage";
const files = await listFiles("resumes");
console.log("Files:", files);โ Summary: Golden Rules
- NEVER fetch from Firebase directly in views โ Always use the type classes in
src/app/types/ - Learn the type classes โ Understanding how to use them for CRUD is crucial
- Use SSR for static pages โ Pages like
/teamand/positionsshould use{ server: true, public: true } - Use
public: truewhen possible โ Reduces Firebase costs by allowing aggressive caching - Storage is for files โ Use Firebase Storage for images, PDFs, resumes, etc.
๐ Quick Reference Table
| What | Where | When to Use |
|---|---|---|
| Type Classes | src/app/types/ | Always (for all Firebase data operations) |
| Client Firestore | src/lib/firebase/client/firestore.ts | Client Components only |
| Server Firestore | src/lib/firebase/server/firestore.ts | Server Components, API routes |
| Storage | src/lib/firebase/client/storage.ts | File uploads/downloads |
| Auth Context | src/contexts/AuthContext.tsx | Check if user is logged in |
| Service Worker | public/worker.template.js | Automatic auth token handling |
๐ Next Steps
- Explore the existing type classes in
src/app/types/ - Practice creating, reading, updating, and deleting data using these classes
- Understand when to use
{ server: true }vs client-side fetching - Understand when to use
{ public: true }to save costs
If you follow these patterns, you'll write clean, efficient, and cost-effective Firebase code!