Why Firebase?
Firebase accelerates app development, cuts costs, and makes the process more enjoyable for developers. As a Backend-as-a-Service (BaaS), Firebase takes care of the heavy lifting on the server side, so you can focus on building a great user experience.
Tasks like user authentication, API calls, security, database management, and traffic scaling typically require time, money, and add complexity to a project. Firebase simplifies all of this by offering a suite of powerful SDKs that directly connect your web or mobile app to Google Cloud services—no need to worry about server maintenance.
By letting Firebase handle the backend, you can invest more resources in creating a top-notch frontend, increasing the chances of your app’s success.
_ Getting Started_
A Firebase project serves as a container for your Google Cloud infrastructure. Each project can support multiple apps, making it common for web, iOS, and Android versions of an app to share the same Firebase project.
To get started, simply create a new project from the Firebase dashboard.
Add Firebase to a Web App
To use Firebase in your web app, you need to add an app to your project. This will generate credentials to connect your app to the cloud. Go to ⚙️ settings and click "Add app."
This will create a config snippet you can use in your Firebase project. Now, let’s set up a simple web app with two files: public/index.html and public/app.js.
In the
of your HTML file, paste the config snippet. Be sure to include script tags for the Auth and Firestore SDKs, as we’ll need these features later. Adding these script tags enhances the core SDK with Firestore and Auth capabilities.💡 In VS Code, type ! and press Tab to quickly generate HTML boilerplate.
In your JavaScript code, you can now use Firebase as a global variable. Log it to the console to ensure everything is working.
console.log(firebase)
Firebase CLI Tools
Now we’re ready to connect our local code to the cloud using Firebase Tools CLI. Run the following commands in your terminal to establish the connection:
npm install -g firebase-tools
firebase login
firebase init
firebase serve
When initializing the project, select Hosting and Emulators. Choose YES for the single-page application option, and accept the defaults for the rest. After running the serve command, you should see your site at http://localhost:5000 in your browser.
💡 Optional: It’s a good idea to install the Firebase Explorer VS Code extension for easier management.
Deploy to Hosting
It’s very satisfying to launch your stuff to the Internet - Firebase makes deployment dead simple.
firebase deploy
Your app is now live on the web at the domains listed in the hosting console.
User Authentication
When starting a new project, I often focus on the user authentication flow first, as many important features require users to be signed in. Firebase Auth offers several methods for user sign-in, but let's start with the simplest option: Google Sign-in.
Add SignIn and SignOut Buttons
First, we need some HTML to create the user interface for signed-in and signed-out users. The signed-in section will be hidden by default. Here's a basic example:
<div id="sign-in-section">
<button id="sign-in-button">Sign in with Google</button>
</div>
<div id="signed-in-section" style="display: none;">
<p>Welcome, <span id="user-name"></span>!</p>
<button id="sign-out-button">Sign Out</button>
</div>
Next, we'll grab the buttons from the HTML and set up event handler functions using onclick. When the sign-in button is clicked, it will use the signInWithPopup method from the Auth SDK to open a window for the user to enter their Google credentials. Firebase will then create a JSON Web Token (JWT) that identifies the user in this browser and keeps them authenticated until the token is invalidated or the user signs out.
Here's how you can do this in your JavaScript:
const signInButton = document.getElementById('sign-in-button');
const signOutButton = document.getElementById('sign-out-button');
const signedInSection = document.getElementById('signed-in-section');
const userName = document.getElementById('user-name');
signInButton.onclick = () => {
// Sign in with Google
const provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider).then((result) => {
const user = result.user;
userName.textContent = user.displayName;
signedInSection.style.display = 'block';
signInButton.style.display = 'none';
}).catch((error) => {
console.error(error);
});
};
signOutButton.onclick = () => {
// Sign out
firebase.auth().signOut().then(() => {
signedInSection.style.display = 'none';
signInButton.style.display = 'block';
}).catch((error) => {
console.error(error);
});
};
Listen to Changes to the Auth State
The onAuthStateChanged method allows you to run a callback function each time the user's authentication state changes. If the user is signed in, the user parameter will be an object containing details like the user's UID and email address. If the user is signed out, it will be null.
Here's how to implement this in your JavaScript:
firebase.auth().onAuthStateChanged((user) => {
if (user) {
// User is signed in
userName.textContent = user.displayName;
signedInSection.style.display = 'block';
signInButton.style.display = 'none';
} else {
// User is signed out
signedInSection.style.display = 'none';
signInButton.style.display = 'block';
}
});
Firestore
Once a user is authenticated, you'll likely want them to perform actions, such as saving records to a database. Firestore is a NoSQL, document-oriented database that is similar to MongoDB. It's easy to manage and flexible, though modeling data relationships can be a bit challenging.
To get started, follow these steps:
- Enable Firestore: Go to the Firebase console.
- Select your project.
- In the left sidebar, click on "Firestore Database."
- Click "Create database" and follow the prompts to enable Firestore.
- Once Firestore is enabled, you can start saving and retrieving data!
Data Model
In this example, we'll use a things collection to create a relationship between the currently signed-in user and Firestore. Each user can have many things, while each thing belongs to one user.
A simple way to model this relationship is to store the user's UID in each document. Here's how you might structure a document in the things collection:
{
"name": "Item Name",
"description": "Description of the item",
"userId": "USER_UID", // This is the UID of the signed-in user
"createdAt": "TIMESTAMP"
}
By including the userId field, you can easily query for all things that belong to a specific user.
Writing to the Database
Now we’ll give our user a way to create records in the database. First, we’ll set up the HTML to render the items stored in Firestore.
<section>
<h2>My Firestore Things</h2>
<ul id="thingsList"></ul>
<button id="createThing">Create a Thing</button>
</section>
This HTML includes a heading, an unordered list to display the items, and a button to create a new item.
app.js
Now, let’s write the JavaScript to handle adding items to Firestore:
const createThing = document.getElementById('createThing');
const thingsList = document.getElementById('thingsList');
const db = firebase.firestore();
let thingsRef;
let unsubscribe;
auth.onAuthStateChanged(user => {
if (user) {
// Database Reference
thingsRef = db.collection('things');
// Add a new document when the button is clicked
createThing.onclick = () => {
const { serverTimestamp } = firebase.firestore.FieldValue;
thingsRef.add({
uid: user.uid,
name: faker.commerce.productName(), // Using Faker to generate a product name
createdAt: serverTimestamp()
}).then(() => {
loadThings(); // Refresh the displayed list after adding
}).catch(error => {
console.error('Error adding document: ', error);
});
};
// Load the user's things
loadThings();
// Optionally, you can add an unsubscribe function if you want to listen for changes
// unsubscribe = thingsRef.onSnapshot(snapshot => {
// // Handle snapshot changes if needed
// });
}
});
// Function to load and display the things
function loadThings() {
thingsList.innerHTML = ''; // Clear the current list
thingsRef.where('uid', '==', firebase.auth().currentUser.uid).get()
.then(querySnapshot => {
querySnapshot.forEach(doc => {
const item = doc.data();
const li = document.createElement('li');
li.textContent = `${item.name} (Created at: ${item.createdAt.toDate().toLocaleString()})`;
thingsList.appendChild(li);
});
}).catch(error => {
console.error('Error getting items: ', error);
});
}
Explanation
- HTML Structure: The HTML provides a simple layout with a heading, a list for items, and a button to create new items.
- Firestore Reference: In app.js, when the user is authenticated, we create a reference to the things collection.
- Creating Items: When the "Create a Thing" button is clicked, a new document is added to the collection. We're using Faker to generate a random product name.
- Loading Items: The loadThings function retrieves items from Firestore that belong to the signed-in user and displays them in the list.
This setup allows users to create and view their items in Firestore seamlessly!
Listening to a Realtime Query
Now that we can write to the database, let’s set up a query to read data and listen for changes in real-time. The onSnapshot method allows us to trigger a callback function whenever the data changes.
Here’s how to implement this in app.js:
let thingsRef;
let unsubscribe;
auth.onAuthStateChanged(user => {
if (user) {
// Database Reference
thingsRef = db.collection('things');
// Query to listen for changes
unsubscribe = thingsRef.where('uid', '==', user.uid)
.onSnapshot(querySnapshot => {
// Map results to an array of <li> elements
const items = querySnapshot.docs.map(doc => {
return `<li>${doc.data().name}</li>`;
});
// Update the items list in the DOM
thingsList.innerHTML = items.join('');
});
} else {
// Unsubscribe when the user signs out
unsubscribe && unsubscribe();
}
});
Explanation
- Database Reference: When the user is authenticated, we create a reference to the things collection.
- Real-time Query: The onSnapshot method is called on the query that filters for documents where the uid matches the current user's UID. This method will fire every time there is a change in the data that meets the query criteria.
- Mapping Results: Inside the callback, we map the results to an array of
- elements. Each element contains the name of the item.
- Updating the DOM: We update the thingsList in the DOM with the new items whenever the data changes.
- Unsubscribing: If the user signs out, we call the unsubscribe function to stop listening for changes.
This setup ensures that the displayed list of items updates in real time as changes occur in the Firestore database!
Composite Indexes
Certain Firestore queries require indexes to be created in order to function properly. If you try to run a query that needs an index, the browser console will throw an error and provide a link to create the necessary index.
When you combine a where method using == with a range operator like < or orderBy, you will need a composite index. For example:
thingsRef
.where('uid', '==', user.uid)
.orderBy('createdAt'); // Requires an index
How to Create an Index
- Run the Query: First, attempt to run your query. If it requires an index, you'll see an error in the console with a link.
- Follow the Link: Click on the link provided in the error message. It will take you to the Firestore console with pre-filled information to create the index.
- Create the Index: Click the "Create" button. Once the index is created, your query will work without issues.
Important Note
Make sure to monitor your Firestore usage and performance, as adding too many indexes can impact your database's efficiency. Use indexes judiciously, especially for queries that will be run frequently.
Security Rules
At some point, it's essential to implement robust server-side security rules for your Firestore database. Without these rules, your app could be vulnerable to exploitation, allowing unauthorized users to access or modify your data.
The following rules ensure that (1) the entire database is locked down by default, and (2) authenticated users can only modify their own data. You can configure these rules from the Firestore console under Database » Rules.
firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Lock down the database
match /{document=**} {
allow read, write: if false;
}
// Allow authorized requests to the things collection
match /things/{docId} {
allow write: if request.auth.uid == request.resource.data.uid;
allow read: if request.auth.uid == resource.data.uid;
}
}
}
Explanation
- Lock Down the Database: The first rule (match /{document=**} { allow read, write: if false; }) ensures that no read or write operations are allowed unless explicitly specified in subsequent rules.
- Allow Access to the things Collection:
- Write Access: The rule allow write: if request.auth.uid == request.resource.data.uid; allows a user to write to a document in the things collection only if the user's UID matches the uid field in the document being written.
- Read Access: The rule allow read: if request.auth.uid == resource.data.uid; permits a user to read a document only if their UID matches the uid field of that document.
Implementation
To implement these rules:
- Open the Firebase console.
- Navigate to Firestore Database.
- Click on the "Rules" tab.
- Replace the existing rules with the above code.
- Publish the rules to apply them.
These security rules will help protect your Firestore database and ensure that users can only access their own data!
The End
That’s it! You've set up user authentication, connected to Firestore, and implemented basic security rules. However, we’ve only scratched the surface of what you can do with Firebase and Firestore.
There are many more features to explore, such as:
- Advanced Queries: Learn how to perform complex queries and aggregations.
- User Roles: Implement role-based access control to manage different user permissions.
- Cloud Functions: Use Cloud Functions to run backend code in response to events triggered by Firestore or other Firebase services.
- Offline Support: Implement offline capabilities to ensure your app works without an internet connection.
Feel free to continue experimenting and building on what you've learned! If you have any questions or need further assistance, don't hesitate to ask. Happy coding!
Top comments (0)