Functionally, this works OK — all the data is there, and it should get the job done. But there are a couple of problems with this model:

If all the fields are in a single document, you have a security problem. Anyone can get the private data, even if the UI doesn’t display it.

If your realtime leaderboard is constantly churning with new high score updates, the bandwidth consumed is way higher than necessary.

If these two points aren’t obvious at first, that’s OK, we’ll dig in and solve them. But first, you’ll need to understand how Firebase pricing works in order to understand the cost of the solutions.

Understanding cost

If you haven’t already, read through the documentation on Firestore’s pricing structure. The general summary is stated at the top of the page.

When you use Cloud Firestore, you are charged for the following: - The number of reads, writes, and deletes that you perform. - The amount of storage that your database uses, including overhead for metadata and indexes. - The amount of network bandwidth that you use.

The one that most developers are concerned about is the number of reads, but the other two points are not to be ignored, especially network bandwidth. If you’re using realtime listeners in your app to display document changes as they occur, you could incur ongoing bandwidth costs, as listeners will stream data across the network (egress) as long as the listener is active.

Protip: If you write code to add a listener, always consider reciprocal code to remove it, else it could leak, and cost you money. Always know when your listeners should be added and removed!

This information about billing needs to be combined with one other fact about Firestore listeners: when a document changes that would cause a listener to receive an update, the entire document is transferred for every change. This means that a change to any individual field will cause the entire document to be sent. For small changes to large documents, this can be unnecessarily costly (and possibly slow).

How a document read actually works

When your client code reads a document from Firestore, either through an individual document get, or through a query, it will read the entire document, with all of its fields. There is currently no way, using web and mobile client SDKs, to perform what’s called a “projection” (using SQL terms). That is to say, you can’t choose which subset of fields get sent across the connection. When you get a document, the entire document is sent every time, regardless of which fields you access from a DocumentSnapshot . The entire document data is always there in memory.

This is still true for Firestore listeners. If you have a listener added to a query, every time a document is added or changed in the results of that query, the entire document is downloaded to the client. But the unchanged or deleted documents are not transferred again — they are reused from the last query snapshot in memory.

These facts have two important ramifications for modeling our player data in the example above.

If anyone can read the document with the screen name and bio (both intended to be public), then anyone can also read the number of credits purchased by that player (intended to be private). If you use a realtime query to monitor the top 100 players by score, and those scores are changing rapidly, then each change in score results in the entire player document be read and downloaded for each change in score, including all the fields that didn’t change.

I’m sure you’re starting to get a sense of why the original model for the players document isn’t optimal: It’s exposing private information, and transmitting too much data. Fortunately, these problems can be resolved (with some caveats).

Address security by splitting up documents

The above model that has all data in a single document can be improved for security purposes by splitting the fields into multiple documents into different public and private subcollections. For example:

/players/{uid}/public/info

- screenName: string

- bio: string

- score: number /players/{uid}/private/credits

- credits: number

With this, you can use security rules to indicate that everyone can read documents in the public subcollections, and only the players can read their own private subcollection. As usual, backend code can make changes to any document, regardless of security rules, so that’s where you’ll want to make changes to a user’s credits.

match /players/{uid}/public/{id} {

// anyone can read a player's public data

allow read: if true;

// but only the player can modify it

allow write: if request.auth.uid == uid;

} match /players/{uid}/private/{id} {

// only a player can read their own private data

allow read: if request.auth.uid == uid;

}

There is a downside to this: it now it requires at least two reads to get all the data about a player, instead of just one.

You can see that improving security can cause an increase in cost and a decrease in performance. And if you ask me, it’s necessary, and totally worth it. You can optimize a bit by choosing to use cached results instead of reading from the server, when it makes sense. It’s also worth mentioning that if you use get() in security rules to read the contents of another document, that also costs a read every time it runs.

Address performance by splitting up documents (again)

I mentioned above that building a realtime dashboard for top player scores involves adding a listener to a query on the collection with the player scores. That listener will receive the entire contents of each matching document any time the score field changes. Firestore does not offer partial reads or delta updates of documents for mobile and web clients.

Take a look at the player document again: