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 Documentation

    1. Firebase Authentication

    How Authentication Works

    Firebase Auth handles user login and signup. We support two methods:

    1. Email/Password: Traditional username and password
    2. Google OAuth: "Sign in with Google" button

    Authentication flow in our project:

    1. User logs in via the login form (src/components/login-form.tsx)
    2. Firebase authenticates them and returns a user object
    3. We store the auth state globally using React Context (src/contexts/AuthContext.tsx)
    4. Protected pages (like /admin) check if the user is logged in
    5. A service worker automatically adds the user's authentication token to requests

    ๐Ÿ“š Official Documentation:

    Firebase Authentication

    Service 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 Sessions

    2. 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 teams

    Server-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: true option 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:

    1. Next.js runs this code on the server
    2. Team.readAll uses the server Firestore library with public access
    3. Data is fetched from Firebase once
    4. The HTML is generated with the data already in it
    5. The HTML is sent to the user's browser
    6. 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

    1. NEVER fetch from Firebase directly in views โ€” Always use the type classes in src/app/types/
    2. Learn the type classes โ€” Understanding how to use them for CRUD is crucial
    3. Use SSR for static pages โ€” Pages like /team and /positions should use { server: true, public: true }
    4. Use public: true when possible โ€” Reduces Firebase costs by allowing aggressive caching
    5. Storage is for files โ€” Use Firebase Storage for images, PDFs, resumes, etc.

    ๐Ÿ“‹ Quick Reference Table

    WhatWhereWhen to Use
    Type Classessrc/app/types/Always (for all Firebase data operations)
    Client Firestoresrc/lib/firebase/client/firestore.tsClient Components only
    Server Firestoresrc/lib/firebase/server/firestore.tsServer Components, API routes
    Storagesrc/lib/firebase/client/storage.tsFile uploads/downloads
    Auth Contextsrc/contexts/AuthContext.tsxCheck if user is logged in
    Service Workerpublic/worker.template.jsAutomatic auth token handling

    ๐Ÿš€ Next Steps

    1. Explore the existing type classes in src/app/types/
    2. Practice creating, reading, updating, and deleting data using these classes
    3. Understand when to use { server: true } vs client-side fetching
    4. Understand when to use { public: true } to save costs

    If you follow these patterns, you'll write clean, efficient, and cost-effective Firebase code!