Content is user-generated and unverified.

Firebase Firestore Reference Guide

Core Concepts and Terminology

Documents

A document is the basic unit of data storage in Firestore. Think of it as a single record that contains fields and values, similar to a row in a spreadsheet or an object in JavaScript. Each document has a unique identifier (ID) within its collection.

javascript
// Example document structure
{
  name: "Alice",
  score: 95,
  lastPlayed: Timestamp.now(),
  preferences: {
    difficulty: "medium",
    soundEnabled: true
  }
}

Collections

A collection is a container for documents, like a folder that holds files. Collections can only contain documents (not other collections directly), and each document in a collection must have a unique ID.

Subcollections

A subcollection is a collection that exists within a specific document. This creates a hierarchical structure. For example, a game document might have a subcollection of players.

games (collection)
  └── gameABC123 (document)
      └── players (subcollection)
          ├── Alice (document)
          └── Bob (document)

References

A reference is a lightweight object that points to a location in your database. References don't contain data—they're like addresses that tell Firestore where to find or place data. You use references to read from or write to specific locations.

Snapshots

A snapshot is the actual data retrieved from Firestore at a specific point in time. When you read a document, you get a snapshot that contains the document's current data and metadata.

Reading Data

To read data from Firestore, you first create a reference to the document or collection you want to read, then use getDoc() for single documents or getDocs() for collections.

Reading a Single Document

javascript
import { doc, getDoc } from 'firebase/firestore';
import { db } from '@/firebase';

async function readPlayerData() {
  // Create a reference to a specific document
  const playerRef = doc(db, 'games', 'gameABC123', 'players', 'Alice');
  
  // Get the snapshot
  const snapshot = await getDoc(playerRef);
  
  // Check if the document exists
  if (snapshot.exists()) {
    // Access the data
    const data = snapshot.data();
    console.log('Player data:', data);
    console.log('Player score:', data.score);
  } else {
    console.log('No such document!');
  }
}

Reading Multiple Documents from a Collection

javascript
import { collection, getDocs } from 'firebase/firestore';
import { db } from '@/firebase';

async function readAllPlayers() {
  // Create a reference to a collection
  const playersRef = collection(db, 'games', 'gameABC123', 'players');
  
  // Get all documents in the collection
  const snapshot = await getDocs(playersRef);
  
  // Iterate through the documents
  snapshot.forEach((doc) => {
    console.log('Player ID:', doc.id);
    console.log('Player data:', doc.data());
  });
}

Writing Data

Writing data involves creating or updating documents. Firestore provides different methods depending on your needs.

Creating or Overwriting a Document

Use setDoc() to create a new document or completely overwrite an existing one:

javascript
import { doc, setDoc, Timestamp } from 'firebase/firestore';
import { db } from '@/firebase';

async function createPlayer() {
  // Create a reference with a specific ID
  const playerRef = doc(db, 'games', 'gameABC123', 'players', 'Charlie');
  
  // Set the document data
  await setDoc(playerRef, {
    name: 'Charlie',
    score: 0,
    joinedAt: Timestamp.now(),
    level: 1
  });
  
  console.log('Player created!');
}

Updating Specific Fields

Use updateDoc() to update only specific fields without overwriting the entire document:

javascript
import { doc, updateDoc } from 'firebase/firestore';
import { db } from '@/firebase';

async function updatePlayerScore() {
  const playerRef = doc(db, 'games', 'gameABC123', 'players', 'Charlie');
  
  // Update only the score field
  await updateDoc(playerRef, {
    score: 150,
    lastUpdated: Timestamp.now()
  });
  
  console.log('Score updated!');
}

Adding a Document with Auto-generated ID

Use addDoc() when you want Firestore to generate a unique ID:

javascript
import { collection, addDoc, Timestamp } from 'firebase/firestore';
import { db } from '@/firebase';

async function addNotification() {
  const notificationsRef = collection(db, 'games', 'gameABC123', 'notifications');
  
  const docRef = await addDoc(notificationsRef, {
    message: 'New achievement unlocked!',
    timestamp: Timestamp.now(),
    read: false
  });
  
  console.log('Notification added with ID:', docRef.id);
}

Practical Examples

Example 1: Game Setup

Creating a new game instance with initial data:

javascript
import { doc, setDoc, Timestamp } from 'firebase/firestore';
import { db } from '@/firebase';

async function setupNewGame(gameCode, hostId) {
  const gameRef = doc(db, 'games', gameCode);
  
  await setDoc(gameRef, {
    metadata: {
      type: 'VirusGame',
      gameCode: gameCode,
      hostId: hostId
    },
    lastPlayed: Timestamp.now(),
    players: [],
    status: 'initializing',
    gameParams: {
      difficulty: 'medium',
      maxPlayers: 30
    }
  });
  
  console.log(`Game ${gameCode} created!`);
}

Example 2: Player Joining a Game

Adding a player to an existing game:

javascript
import { doc, updateDoc, arrayUnion, setDoc, Timestamp } from 'firebase/firestore';
import { db } from '@/firebase';

async function joinGame(gameCode, playerName, userId) {
  // First, add player name to the game's players array
  const gameRef = doc(db, 'games', gameCode);
  await updateDoc(gameRef, {
    players: arrayUnion(playerName)
  });
  
  // Then, create the player document in the subcollection
  const playerRef = doc(db, 'games', gameCode, 'players', playerName);
  await setDoc(playerRef, {
    name: playerName,
    currentUid: userId,
    joinedAt: Timestamp.now(),
    approved: false,
    score: 0
  });
  
  console.log(`${playerName} joined game ${gameCode}!`);
}

Example 3: Reading and Updating Game State

Checking game status and updating it:

javascript
import { doc, getDoc, updateDoc } from 'firebase/firestore';
import { db } from '@/firebase';

async function startGameIfReady(gameCode) {
  const gameRef = doc(db, 'games', gameCode);
  const snapshot = await getDoc(gameRef);
  
  if (!snapshot.exists()) {
    console.error('Game not found!');
    return;
  }
  
  const gameData = snapshot.data();
  
  // Check if we can start the game
  if (gameData.status === 'initializing' && gameData.players.length >= 2) {
    await updateDoc(gameRef, {
      status: 'running',
      gameStartedAt: Timestamp.now()
    });
    console.log('Game started!');
  } else {
    console.log('Game not ready to start');
  }
}

Project-Specific Infrastructure

This project provides several conveniences that make working with Firebase much easier:

Automatic Data Synchronization

The most important thing to understand is that you rarely need to read data directly from Firestore. The project automatically keeps a Zustand store synchronized with all relevant Firestore data. This means you can simply read from the store in your components:

javascript
// Instead of manually reading from Firestore, use the game-specific store hook
const playerScore = useVirusGameStore(state => state.playerData.score);
const allPlayers = useVirusGameStore(state => state.players);
const stations = useVirusGameStore(state => state.subcollections.stations);

The data in the store updates automatically whenever the Firestore data changes, so your components will always show the latest information.

Helper Functions for References

The project provides convenient helper functions in refs.ts to get commonly-used references without manually constructing paths:

javascript
import { gameDoc, playerDoc, gameSubcol } from '@/firebase';

// Get a reference to a game document
const gameRef = gameDoc<'VirusGame'>('ABC123');

// Get a reference to a player document
const playerRef = playerDoc<'VirusGame'>('ABC123', 'Alice');

// Get a reference to a subcollection
const stationsRef = gameSubcol<'VirusGame'>('ABC123')('stations');

These helpers ensure type safety and prevent errors from incorrect paths.

Writing Data Pattern

While reading is handled by the store, you still write directly to Firestore using the standard Firebase methods. The typical pattern is:

  1. Read current state from the store (not Firestore)
  2. Calculate what needs to change
  3. Write changes to Firestore
  4. The store automatically updates, triggering component re-renders
javascript
import { updateDoc } from 'firebase/firestore';
import { gameDoc } from '@/firebase';

function MyGameComponent() {
  const gameCode = useGameCode();
  const currentScore = useVirusGameStore(state => state.gameData.totalScore);
  
  const incrementScore = async () => {
    // Write directly to Firestore
    await updateDoc(gameDoc<'VirusGame'>(gameCode), {
      totalScore: currentScore + 10
    });
    // No need to update local state - the store will sync automatically!
  };
  
  return <button onClick={incrementScore}>Add 10 Points</button>;
}

Type Safety

The project uses TypeScript to ensure type safety throughout. When you use the game-specific store hook or helper functions with the game type specified, TypeScript will:

  • Autocomplete available fields
  • Prevent accessing non-existent properties
  • Ensure you're writing the correct data types to Firestore

Available Data by Component Type

Different components have access to different data based on their role:

  • Player components: Can access game data, their own player data, and subcollections
  • Teacher components: Can access game data, all players' data, and subcollections
  • Display components: Can access game data, all players' data, and subcollections

This is handled automatically by the useGameDataSync hook that runs in the background.

Key Takeaways for Development

  1. Read from the store, write to Firestore - Let the synchronization handle updates
  2. Use the provided helper functions for creating references to avoid path errors
  3. Trust the type system - TypeScript will guide you to use the correct data structures
  4. Don't worry about synchronization - Focus on your game logic; the infrastructure handles the rest

This infrastructure means you can focus on building your game features without worrying about the complexities of real-time data synchronization or manual state management.

Content is user-generated and unverified.
    Firebase Firestore Reference Guide | Claude