Designing your data
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:
- PlayersOne per account: a nickname and when they joined.
- RunsOne finished game: score, wave reached, how long it lasted. A player has many runs.
- ScoresLeaderboard entries. Each one belongs to one player and one day.
- ItemsHats and seeds. The shop catalogue is the same for everyone. What each player owns is not.
Shape it for your reads
Next, list the questions the game asks, and how often:
- "Load my save": every time the game starts.
- "Show today's top 10": thousands of times a day.
- "Show my last 20 runs": now and then.
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?
- A runs list inside players/{uid}, growing with every run
- A subcollection, players/{uid}/runs, with one document per run
- 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
1{2 uid: "k3P9xQ2m",3 name: "SwiftComet",4 score: 48200,5 day: "2026-10-09"6}
This score copies the pilot's name on line 3. If SwiftComet renames to NovaQueen, Firestore updates the old scores automatically.
- True
- False
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:
- SQL fitsLots of connected data and new questions all the time: orders, refunds, reports.
- Firestore fitsData read the same way again and again: one save per player, live boards, offline play.
SELECT s.score, p.nickname FROM scores sJOIN 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:
- Pick IDs on purposeUse the UID as each player document's ID, so a security rule can match it. Use automatic IDs for scores: Firebase warns that counting-up IDs like score1, score2 create hotspots that slow writes.
- Server timeRecord when things happened with serverTimestamp(), the server's clock. A device clock can be wrong, or changed by a cheater.
- Only what you needNo passwords, keys or real names, and no personal data in IDs or field names. Less data, less to protect (see Keeping player data safe).
- A version fieldEvery save holds version: 1. When the shape changes, the game upgrades old saves as it loads them, one step at a time: 1 to 2, then 2 to 3. This is a save migration.
Old save
1function upgrade(save) {2 if (save.version === 1) {3 save.hat = save.sprigHat;4 delete save.sprigHat;5 save.version = 2;6 }7 return save;8}
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?
- { version: 2, wave: 9, hat: "acorn" }
- The same save, still version 1
- 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
- Firestore: data model firebase.google.com
- Firestore: choose a data structure firebase.google.com
- Firestore: aggregation queries firebase.google.com
- Firestore: best practices firebase.google.com
- Firebase Realtime Database: structure your data firebase.google.com
- Firebase SQL Connect firebase.google.com
- IBM: the relational database ibm.com