Cloud Firestore is a cloud-hosted, NoSQL database that your iOS, Android, and web apps can access directly via native SDKs.
Initialize Cloud Firestore
Get Firebase configuration from web console and then use it to initialize the app in your web frontend:
// Initialize Cloud Firestore through Firebase firebase.initializeApp({ apiKey: '### FIREBASE API KEY ###', authDomain: '### FIREBASE AUTH DOMAIN ###', projectId: '### CLOUD FIRESTORE PROJECT ID ###' }); var db = firebase.firestore();
A collection contains documents and nothing else. Documents contain data as property-value pairs. The names of documents within a collection are unique. A document can have a subcollection itself. Documents are schema-less, meaning that within a collection each document have different properties. However, it’s a good idea to use the same fields and data types across multiple documents, so that you can query the documents more easily.
Data Types
- Array. Cannot contain array itself
- Boolean
- Bytes
- Date and time ()
- Floating point number (Cloud Firestore always stores numbers as doubles)
- Geographical point
- Integer
- Map
- Null
- Reference. By path elements such as
projects/[PROJECT_ID]/databases/[DATABASE_ID]/documents/[DOCUMENT_PATH]
- Text string
var docData = { stringExample: "Hello world!", booleanExample: true, numberExample: 3.14159265, dateExample: firebase.firestore.Timestamp.fromDate(new Date("December 10, 1815")), arrayExample: [5, true, "hello"], nullExample: null, objectExample: { a: 5, b: { nested: "foo" } } };
SDK
Cloud Firestore supports mobile/web SDKs and server client libraries which implement best practices for you and make it easier to access your database. Clients connect directly to your Cloud Firestore database without intermediary server. The mobile and web SDKs also support realtime updates and offline data persistence.
The server client libraries (aka Firebase Admin SDK) create a privileged Cloud Firestore environment with full access to your database. Requests are not evaluated against your Cloud Firestore security rules, instead they are secured via Identity and Access Management (IAM).
References
A reference is a lightweight object that just points to a location in your database. You can create a reference whether or not data exists there, and creating a reference does not perform any network operations.
// Reference a document let docRef = db.collection('users').doc('alovelace'); // Reference a collection let usersRef = = db.collection('users'); // use a path to a document let alovelaceDocumentRef = db.doc('users/alovelace'); // reference sub collection var messageRef = db.collection('rooms').doc('roomA').collection('messages').doc('message1');
Adding documents
You do not need to “create” or “delete” collections. After you create the first document in a collection, the collection exists. If you delete all of the documents in a collection, it no longer exists.
Option 1: With auto-generated ID (collection.add)
Cloud Firestore auto-generated IDs do not provide any automatic ordering. If you want to be able to order your documents by creation date, you should store a timestamp as a field in the documents.
db.collection("users").add({ first: "Ada", last: "Lovelace", born: 1815 }) .then((docRef) => { console.log("Document written with ID: ", docRef.id); }) .catch((error) => { console.error("Error adding document: ", error); });
Option 2: Create a ref (collection.doc) then ref.set
// Add a new document with a generated id. var newCityRef = db.collection("cities").doc(); // later... newCityRef.set(data);
With specific ID (collection.doc(id))
db.collection("cities").doc("new-city-id").set(data);
Create or overwrite document (doc.set)
db.collection("cities").doc("LA").set({ name: "Los Angeles", state: "CA", country: "USA" }) .then(() => { console.log("Document successfully written!"); }) .catch((error) => { console.error("Error writing document: ", error); });
Create or update document (doc.set with merge)
set
with merge
will update fields in the document or create it if it doesn’t exists. For set
you always have to provide document-shaped data:
var cityRef = db.collection('cities').doc('BJ'); var setWithMerge = cityRef.set( {a: {b: {c: true}}}, {merge: true} );
Update document and fail if exists (doc.update)
With update
you can also use field paths for updating nested values:
update({ 'a.b.c': true })
Important difference between this dot notation and without: With dot notation you update a single nested field without overwriting other nested field. If you update a nested field without dot notation, you will overwrite the entire map field.
Add/Remove elements in an array (arrayUnion/arrayRemove)
arrayUnion()
adds elements to an array but only elements not already present. arrayRemove()
removes all instances of each given element.
var washingtonRef = db.collection("cities").doc("DC"); // Atomically add a new region to the "regions" array field. washingtonRef.update({ regions: firebase.firestore.FieldValue.arrayUnion("greater_virginia") }); // Atomically remove a region from the "regions" array field. washingtonRef.update({ regions: firebase.firestore.FieldValue.arrayRemove("east_coast") });
Server Timestamp
Let server set timestamp when updating a document. When updating multiple timestamp fields inside of a transaction, each field receives the same server timestamp value.
var docRef = db.collection('objects').doc('some-id'); // Update the timestamp field with the value from the server var updateTimestamp = docRef.update({ timestamp: firebase.firestore.FieldValue.serverTimestamp() });
Increment numeric value
var washingtonRef = db.collection('cities').doc('DC'); // Atomically increment the population of the city by 50. washingtonRef.update({ population: firebase.firestore.FieldValue.increment(50) });
Delete documents
Deleting a document does not delete its subcollections.
db.collection("cities").doc("DC").delete().then(() => { console.log("Document successfully deleted!"); }).catch((error) => { console.error("Error removing document: ", error); });
Delete fields
var cityRef = db.collection('cities').doc('BJ'); // Remove the 'capital' field from the document var removeCapital = cityRef.update({ capital: firebase.firestore.FieldValue.delete() });
Delete collections
Deleting collections from a Web client is not recommended. Instead do so only from a trusted server environment
async function deleteCollection(db, collectionPath, batchSize) { const collectionRef = db.collection(collectionPath); const query = collectionRef.orderBy('__name__').limit(batchSize); return new Promise((resolve, reject) => { deleteQueryBatch(db, query, resolve).catch(reject); }); } async function deleteQueryBatch(db, query, resolve) { const snapshot = await query.get(); const batchSize = snapshot.size; if (batchSize === 0) { // When there are no documents left, we are done resolve(); return; } // Delete documents in a batch const batch = db.batch(); snapshot.docs.forEach((doc) => { batch.delete(doc.ref); }); await batch.commit(); // Recurse on the next process tick, to avoid // exploding the stack. process.nextTick(() => { deleteQueryBatch(db, query, resolve); }); }
Delete data using Firebase CLI
firebase firestore:delete [options] <<path>>
Reading
Two approaches:
- Call a method to get the data or
- set a listener to receive data-change events, then Cloud Firestore sends your listener an initial snapshot of the data, and then another snapshot each time the document changes
Get single document / check if document exists (ref.get())
var docRef = db.collection("cities").doc("SF"); docRef.get().then((doc) => { if (doc.exists) { console.log("Document data:", doc.data()); } else { // doc.data() will be undefined in this case console.log("No such document!"); } }).catch((error) => { console.log("Error getting document:", error); });
Get from database or cache
By default, a get
call will attempt to fetch the latest document snapshot from your database. On platforms with offline support, the client library will use the offline cache if the network is unavailable or if the request times out.
var docRef = db.collection("cities").doc("SF"); // Valid options for source are 'server', 'cache', or // 'default'. See https://firebase.google.com/docs/reference/js/firebase.firestore.GetOptions // for more information. var getOptions = { source: 'cache' }; // Get a document, forcing the SDK to fetch from the offline cache. docRef.get(getOptions).then((doc) => { // Document was found in the cache. If no cached document exists, // an error will be returned to the 'catch' block below. console.log("Cached document data:", doc.data()); }).catch((error) => { console.log("Error getting cached document:", error); });
Get multiple documents from a collection (collection.where())
db.collection("cities").where("capital", "==", true) .get() .then((querySnapshot) => { querySnapshot.forEach((doc) => { // doc.data() is never undefined for query doc snapshots console.log(doc.id, " => ", doc.data()); }); }) .catch((error) => { console.log("Error getting documents: ", error); });
Retrieve the entire collection (collection.get):
db.collection("users").get().then((querySnapshot) => { querySnapshot.forEach((doc) => { console.log(`${doc.id} => ${doc.data()}`); }); });
Retrieve collection group (db.collectionGroup)
If each document in your cities
collection has a subcollection called landmarks
, all of the landmarks
subcollections belong to the same collection group
var museums = db.collectionGroup('landmarks').where('type', '==', 'museum'); museums.get().then((querySnapshot) => { querySnapshot.forEach((doc) => { console.log(doc.id, ' => ', doc.data()); }); });
Receiving realtime updates / snapshots (doc.onSnapshot)
db.collection("cities").doc("SF") .onSnapshot((doc) => { console.log("Current data: ", doc.data()); });
Latency compensation: When you perform a write, your listeners will be notified with the new data before the data is sent to the backend. Retrieved documents have a metadata.hasPendingWrites
property that indicates whether the document has local changes that haven’t been written to the backend yet. You can use this property to determine the source of events received by your snapshot listener:
db.collection("cities").doc("SF") .onSnapshot((doc) => { var source = doc.metadata.hasPendingWrites ? "Local" : "Server"; console.log(source, " data: ", doc.data()); });
By default, listeners are not notified of changes that only affect metadata, but if you really need to, you can enable it:
db.collection("cities").doc("SF") .onSnapshot({ // Listen for document metadata changes includeMetadataChanges: true }, (doc) => { // ... });
Stop listening for realtime updates / snapshots
By the way: After an error, the listener will not receive any more events, and there is no need to detach your listener.
var unsubscribe = db.collection("cities") .onSnapshot(() => { // Respond to data // ... }); // Later ... // Stop listening to changes unsubscribe();
Handle listen errors
db.collection("cities") .onSnapshot((snapshot) => { // ... }, (error) => { // ... });
Listen to multiple documents in a collection
You can listen to the results of a query. This creates a query snapshot. With this, the snapshot handler will receive a new query snapshot every time the query results change (that is, when a document is added, removed, or modified):
db.collection("cities").where("state", "==", "CA") .onSnapshot((querySnapshot) => { var cities = []; querySnapshot.forEach((doc) => { cities.push(doc.data().name); }); console.log("Current cities in CA: ", cities.join(", ")); });
View changes between snapshots
It is often useful to see the actual changes to query results between query snapshots, instead of simply using the entire query snapshot. For example, you may want to maintain a cache as individual documents are added, removed, and modified.
db.collection("cities").where("state", "==", "CA") .onSnapshot((snapshot) => { snapshot.docChanges().forEach((change) => { if (change.type === "added") { console.log("New city: ", change.doc.data()); } if (change.type === "modified") { console.log("Modified city: ", change.doc.data()); } if (change.type === "removed") { console.log("Removed city: ", change.doc.data()); } }); });
Perform simple and compound queries
db.collection("cities").where("capital", "==", true) .get() .then((querySnapshot) => { querySnapshot.forEach((doc) => { // doc.data() is never undefined for query doc snapshots console.log(doc.id, " => ", doc.data()); }); }) .catch((error) => { console.log("Error getting documents: ", error); });
citiesRef.where("state", "==", "CA"); // combine up to 10 equality (==) clauses with logical OR citiesRef.where('country', 'in', ['USA', 'Japan']); citiesRef.where("population", "<", 100000); citiesRef.where("name", ">=", "San Francisco"); // This query does not return city documents where the capital field does not exist citiesRef.where("capital", "!=", false); // combine up to 10 not equal (!=) clauses with logical OR citiesRef.where('country', 'not-in', ['USA', 'Japan']); // by array content citiesRef.where("regions", "array-contains", "west_coast"); // combine up to 10 array-contains clauses citiesRef.where('regions', 'array-contains-any', ['west_coast', 'east_coast']); // unlike array-contains-any, the clause matches for an exact match of array length, order, and values citiesRef.where('region', 'in', [['west_coast', 'east_coast']]);
Another way to query for document ids using where:
db.collection('books').where(firebase.firestore.FieldPath.documentId(), '==', 'fK3ddutEpD2qQqRMXNW5').get()
Compound queries
Chaining multiple where queries together will join them with logical AND.
citiesRef.where("state", "==", "CO").where("name", "==", "Denver"); citiesRef.where("state", "==", "CA").where("population", "<", 1000000); // This does not work because you have range filters on multiple fields citiesRef.where("state", ">=", "CA").where("population", ">", 100000);
Restrictions to make this work:
- You must create a composite index to combine equality operators with the inequality operators,
<
,<=
,>
, and!=
- You can perform range (
<
,<=
,>
,>=
) or not equals (!=
) comparisons only on a single field - you can include at most one
array-contains
orarray-contains-any
Order and limit
orderBy
can also be used as a filter. For example, if you order by a field that does not exist on a doc in a collection, then that doc is filtered out from the result.
citiesRef.where("population", ">", 100000).orderBy("population").limit(2);
If what you are ordering is a map, you can even order by a property of the map:
db.collection('books').orderBy('review.john-doe');
Paginate a query (collection.startAfter, startAt)
startAfter
is exclusive, meaning it gets all data after the startAfter
condition, but not including it, whereas startAt
is inclusive. endBefore
and endAt
also exist.
var first = db.collection("cities") .orderBy("population") .limit(25); return first.get().then((documentSnapshots) => { // Get the last visible document var lastVisible = documentSnapshots.docs[documentSnapshots.docs.length-1]; console.log("last", lastVisible); // Construct a new query starting at this document, // get the next 25 cities. var next = db.collection("cities") .orderBy("population") .startAfter(lastVisible) .limit(25); });
Securing data via rules
To secure data access you define rules, either in Firebase Web Console or you define them locally in a database.rules.json
that you then deploy.
// Allow read/write access on all documents to any user signed in to the application service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if request.auth != null; } } }
// Deny read/write access to all users under any conditions service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if false; } } }
// Allow read/write access to all users under any conditions // Warning: **NEVER** use this rule set in production; it allows // anyone to overwrite your entire database. service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read, write: if true; } } }
Transactions and Batch Writes
Difference: Transcaction is a set of read and write operations on one or up to 500 documents. Batch Writes is a set of write operations on one or up to 500 documents.
Transactions (db.runTransaction)
- Read operations must come before write operations. There are no read operations in a batch write
- A function calling a transaction (transaction function) might run more than once if a concurrent edit affects a document that the transaction reads. That does not apply to batch writes.
- Transaction functions should not directly modify application state. Instead return the values that change application state
- Transactions will fail when the client is offline, batch writes do no.
// Create a reference to the SF doc. var sfDocRef = db.collection("cities").doc("SF"); // Uncomment to initialize the doc. // sfDocRef.set({ population: 0 }); return db.runTransaction((transaction) => { // This code may get re-run multiple times if there are conflicts. return transaction.get(sfDocRef).then((sfDoc) => { if (!sfDoc.exists) { throw "Document does not exist!"; } // Add one person to the city population. // Note: this could be done without a transaction // by updating the population using FieldValue.increment() var newPopulation = sfDoc.data().population + 1; transaction.update(sfDocRef, { population: newPopulation }); }); }).then(() => { console.log("Transaction successfully committed!"); }).catch((error) => { console.log("Transaction failed: ", error); });
Batch Writes (db.batch, batch.commit)
// Get a new write batch var batch = db.batch(); // Set the value of 'NYC' var nycRef = db.collection("cities").doc("NYC"); batch.set(nycRef, {name: "New York City"}); // Update the population of 'SF' var sfRef = db.collection("cities").doc("SF"); batch.update(sfRef, {"population": 1000000}); // Delete the city 'LA' var laRef = db.collection("cities").doc("LA"); batch.delete(laRef); // Commit the batch batch.commit().then(() => { // ... });
Access data offline
With offline persistence enabled, the Cloud Firestore client library automatically manages online and offline data access and synchronizes local data when the device is back online. For the web, offline persistence is disabled by default and is supported only by the Chrome, Safari, and Firefox web browsers. To enable persistence, call the enablePersistence
method:
firebase.firestore().enablePersistence() .catch((err) => { if (err.code == 'failed-precondition') { // Multiple tabs open, persistence can only be enabled // in one tab at a a time. // ... } else if (err.code == 'unimplemented') { // The current browser does not support all of the // features required to enable persistence // ... } }); // Subsequent queries will use persistence, if it was enabled successfully
Cache size
After exceeding the default cache size of 40 MB, Cloud Firestore periodically attempts to clean up older, unused documents. You can configure a different cache size threshold or disable the clean-up process completely:
// The default cache size threshold is 40 MB. Configure "cacheSizeBytes" // for a different threshold (minimum 1 MB) or set to "CACHE_SIZE_UNLIMITED" // to disable clean-up. firebase.firestore().settings({ cacheSizeBytes: firebase.firestore.CACHE_SIZE_UNLIMITED }); firebase.firestore().enablePersistence()
Listen to offline data
While the device is offline, if you have enabled offline persistence, your listeners will receive listen events when the locally cached data changes. You can listen to documents, collections, and queries. To check whether you’re receiving data from the server or the cache, use the fromCache
property on the SnapshotMetadata
in your snapshot event. If fromCache
is true
, the data came from the cache and might be stale or incomplete. If fromCache
is false
, the data is complete and current with the latest updates on the server.
Disable and enable network access
You can use the method below to disable network access for your Cloud Firestore client. While network access is disabled, all snapshot listeners and document requests retrieve results from the cache. Write operations are queued until network access is re-enabled.
firebase.firestore().disableNetwork() .then(() => { // Do offline actions // ... });
firebase.firestore().enableNetwork() .then(() => { // Do online actions // ... });
Data modelling
How to store your data is a matter of how you expect your data to be used. Things you have to consider:
- A doc has a 1 MB limit
- A doc has a limit of max 40.000 fields
- You don’t pay for how much data you store, instead you pay for every read or write
Ask yourself: How many items should be in the set:
- one-to-few then you can consider embedding your data in a document
- one-to-hundreds, depends on the data but you should tend to create a collection or subcollection
- one-to-billions then you create a collection or subcollection
Also ask yourself: Is the data public or private?
If the data is private, then put it in a separate collection and apply restricted data security rules to it and
- either use the same docID in both collections
- or if you do not want to use the same doc ID, then save the ID of the private doc in a custom field of the public doc
Also ask yourself: Do I need to query data across multiple parents?
If you need to query data often then don’t embed in in a doc, because you would have to read all docs and then do client-side filtering. Instead create a collection or subcollection. For example, of you want to get all books of a specific author, you would put author and books in their own collection and add a author field to the docs in the book collection.
Embedding data in your doc
You embed tags directly in your doc:
"myPost" { title: "my title", tags: [{name: "my tag A"}, {name: "my tag B"}, {name: "my tag C"}] }
Very performant and cost effective method. Your data must be small enough not to reach the 1MB per doc limit. You cannot query the embedded docs but instead have to read the entire doc every time you access it.
Root collection
Let’s assume you want to save many tags for one post. Each tag is a document having their own ID (tagIdA, tagIdB etc.) in a root collection. We then reference one or more tagIds within a post using those ids. We have a many-to-many relationship if we store many tagIds in an array, because a post can reference many tags and a tag can be referenced by many posts. To model a one-to-one relationship we only store one tagId in the post.
"myPost" { title: "my title", tags: [tagIdA, tagIdB, tagIdC] // is an array // or use tag: tagIdA for one-to-one relationship } "tagsCollection" [ "tagIdA" { name: 'my tag A' }, "tagIdB" { name: 'my tag B' }, "tagIdC" { name: 'my tag C' }, ]
A disadvantage may be that you have to make at least two reads to get a small number of data: one for the post and another for its tags.
Subcollections
A subcollection of a doc creates an implicit one-to-many relationship: One post with many tags (as subcollection). The interesting thing is that we only read the tags if we request the tags-subcollection specifically (whereas with Embedding technique we always would get the whole data set).
"myPost" { title: "my title", tags: [tagIdA, tagIdB, tagIdC] // is a subcollection }
You can query subcollections by using Subcollection Groups.
Bucketing
Your main data is held in a one collection with a doc having “docID”. Then tags related to that doc are stored in another collection having a doc Id which is also “docID”. In short: posts -> “docID” refers to tags -> “docID”.
tags [ myFirstPostId { tags: [ tagIdA: {name : 'my tag a'}, tagIdB: {name : 'my tag b'} ] } myPostBId ] posts [ myFirstPostId ]
It requires two reads to get all the tags for one post.