Designing your data

Play this lesson

Plan the data first

Kai wants to start coding cloud saves today. Remy stops them: "If every screen invents its own data, the shop saves hat, the save screen saves sprigHat, and nothing matches. Fixing that later means moving every player's data."

So studios plan a data model first: what things exist, what each one holds, and how they connect. Start by listing the nouns:

Shape it for your reads

Next, list the questions the game asks, and how often:

A read means fetching one document from the database, and Firestore bills per document read. So we shape the data so the most common questions need the fewest reads, even if writing gets a little more work. A score is written once, but read by everyone who opens the board.

Bolt says: Write down the screens first. The data plan grows out of the questions those screens ask.

Collections, documents, subcollections

Firestore, Firebase's database, is a NoSQL database. It stores documents (cards with fields like wave: 12) inside collections (folders). The cloud saves lesson shows the code.

A document can also hold a subcollection, a folder that belongs to one card: players/k3P9xQ2m/runs/r81. Firebase suggests subcollections for lists that keep growing, and lists inside the document for small, fixed ones.

Two surprises from Firebase's docs: reading a player document doesn't download its subcollections, and deleting it doesn't delete them either.

Pick the shape

Hyper Shards will keep every run a pilot ever plays, maybe thousands. Where should the runs go?

  1. A runs list inside players/{uid}, growing with every run
  2. A subcollection, players/{uid}/runs, with one document per run
  3. One document called allRuns that holds every pilot's runs
Show the answer

A subcollection, players/{uid}/runs, with one document per run

A subcollection can keep growing, and a pilot's runs load only when the history screen asks for them. A list inside the player document makes every save load bigger and heads for Firestore's 1 MiB document limit. One shared document hits that limit even faster.

Copy on purpose: denormalising

The leaderboard shows names, but a score only stores the player's uid. To show the top 10, the game reads 10 scores, then 10 player documents for the names: 20 reads.

So we copy the nickname into each score when it is written. Now the board needs 10 reads. Copying data on purpose to make reads cheaper is called denormalising. Firebase's own guides do it, for example keeping a restaurant's average rating on the restaurant document instead of reading every rating.

The trade-off: copies go out of date. Copy small fields that are read often and change rarely.

Rename trouble

scores/x7Qa
{  uid: "k3P9xQ2m",  name: "SwiftComet",  score: 48200,  day: "2026-10-09"}

This score copies the pilot's name on line 3. If SwiftComet renames to NovaQueen, Firestore updates the old scores automatically.

Show the answer

False

False. Firestore has no idea line 3 is a copy. Old scores keep SwiftComet until our own code, often a Cloud Function, finds and updates them, or the team decides old scores keep the old name. That is the price of a cheaper board.

SQL tables or NoSQL documents?

The other big family is SQL (relational) databases, based on a 1970 IBM paper by Edgar F. Codd. Data lives in tables of rows and columns, and every row has the same columns. Tables connect through IDs, called relations, and a join combines them in one query:

SELECT s.score, p.nickname FROM scores s
JOIN players p ON s.uid = p.uid;

Each fact is stored once, so a rename is one change. Standard Firestore queries have no joins, which is why we copy names.

Bolt says: Firebase has a SQL option too: Firebase SQL Connect (formerly Data Connect), which runs a PostgreSQL database on Google's Cloud SQL.

IDs, time and a version number

Four small decisions that are hard to change later:

Old save

migrate.js
function upgrade(save) {  if (save.version === 1) {    save.hat = save.sprigHat;    delete save.sprigHat;    save.version = 2;  }  return save;}

Version 2 of Garden Defense renamed sprigHat to hat. A player returns with last year's save: { version: 1, wave: 9, sprigHat: "acorn" }. What does upgrade() give back?

  1. { version: 2, wave: 9, hat: "acorn" }
  2. The same save, still version 1
  3. A new save that starts at wave 1
Show the answer

{ version: 2, wave: 9, hat: "acorn" }

Line 2 spots a version 1 save, line 3 copies the hat to its new name, line 4 removes the old field and line 5 marks it version 2. The wave is untouched, so the player keeps both their progress and their acorn hat.

A plan before the code

Our plan: players/{uid} holds each save with a version number. players/{uid}/runs keeps the run history. scores holds leaderboard entries, each with a copied nickname and a server timestamp.

Every one of those is filed under a UID, the player's user ID. Next: where that UID comes from, by logging players in.

Bolt says: Before writing database code, ask: what do the busiest screens read, and how many reads does that take?

Fact-checked 9 October 2026