Up and running in a breeze with Firebase

Krzysztof Rzymkowski
Pragmatists
Published in
9 min readMar 13, 2020

--

A typical web application needs just a few things: some place to host static files, some way to store user data, and the ability to log in. Eventually, there’s often a need for a bit of server-side code. This post covers the infrastructure requirements for the vast majority of web apps.

All these needs are fulfilled by Google Firebase. It’s a platform that provides easy-to-use services for web and mobile application development.

In this post, I’ll guide you in building and deploying an as-simple-as-possible web application on Firebase. The app will include persistence with notifications on data changes, authentication using Google accounts, simple server-side functions, and will be hosted in a region of your choice. Best of all, this exercise doesn’t require anything above the free tier. No need to pull out your credit card.

Let’s dig in. You will need to have Node v8 installed and a Google account. (Node v10 support is in beta at the time of writing).

The only way to create your first Firebase project is via https://console.firebase.google.com/. That’s because you’ll need to accept Terms and Conditions there. The process is really straightforward.

After your project is created, you need to do one more thing in the web console. You’ll need to select the Google Cloud Platform region where this project will be hosted:

We can switch now to the command line tool. There are a few ways to install it. I’ll be using installation via npm, as we’ll be using Node anyway:

npm i -g firebase-tools

After that, you’ll have the firebase command at your disposal. Let’s log in with:

firebase login

This will pop up a browser window asking for permission to your Google account.

Now, let’s bootstrap a Firebase project. Go to an empty directory and run the following:

firebase init

You’ll be greeted with a colorful console wizard. For now, just pick Hosting:

? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices. 
◯ Database: Deploy Firebase Realtime Database Rules
◯ Firestore: Deploy rules and create indexes for Firestore
◯ Functions: Configure and deploy Cloud Functions
❯◉ Hosting: Configure and deploy Firebase Hosting sites
◯ Storage: Deploy Cloud Storage security rules
◯ Emulators: Set up local emulators for Firebase features

Then select the project you’ve created in the web console:

? Please select an option: (Use arrow keys)
❯ Use an existing project
Create a new project
Add Firebase to an existing Google Cloud Platform project
Don't set up a default project

The last non-default answer is yes for a single-page app configuration:

? Configure as a single-page app (rewrite all urls to /index.html)? (y/N) y

Let’s see what we’ve got:

├── firebase.json
├── .firebaserc
└── public
└── index.html

Project scaffolding resulted in just a few simple config files. .firebaserc only holds the aliases of the Firebase project identifiers. The other file — firebase.json— is more interesting and is the entry point for the configuration of all services. We’ll get back to it as we go along.

Now, let’s create a frontend. Any framework could do, but I’ll be using my favorite — React. Let’s install the create react app and create a new project in thewebapp directory:

npm i -g create-react-app
create-react-app webapp --typescript

The frontend bundle will be created in webapp/build using npm run build. We can tell the firebase command that this is needed to deploy the application. Open firebase.json, change the public directory and add a predeploy step as shown below:

{
"hosting": {
"predeploy": [
"npm --prefix webapp run build"
],
"public": "webapp/build",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
}
}

After that, you can build and deploy the project using a single command:

firebase deploy

The output will end with:

...
✔ hosting: Finished running predeploy script.
i hosting[your-project-id]: beginning deploy...
i hosting[your-project-id]: found 19 files in webapp/build
✔ hosting[your-project-id]: file upload complete
i hosting[your-project-id]: finalizing version...
✔ hosting[your-project-id]: version finalized
i hosting[your-project-id]: releasing new version...
✔ hosting[your-project-id]: release complete

✔ Deploy complete!

Project Console: https://console.firebase.google.com/project/your-project-id/overview
Hosting URL: https://your-project-id.firebaseapp.com

Now, the bundled React application is accessible at https://your-project-id.firebaseapp.com as well as at https://your-project-id.web.app.

Using Firebase services

Nothing special so far. There’s a plethora of places where you can host a static frontend app for free. What makes Firebase stand out are the various services it provides. To use them from our web app, we first need to include firebase npm dependency:

npm --prefix webapp i firebase

The line above is equivalent to cd webapp && npm i firebase. I’ll be using --prefix in order to remain in the project root directory.

We need to associate our application with a specific Firebase project. To do this, register a web app to generate the necessary keys:

$ firebase apps:create
? Please choose the platform of the app:
iOS
Android
❯ Web
? What would you like to call your app? webapp

To actually obtain the keys, run firebase apps:sdkconfig and paste the output to webapp/src/firebase.ts, like this:

import firebase from "firebase/app";

firebase.initializeApp({
"projectId": "fir-blog-9304f",
"appId": "1:388751691931:web:9752de94c07db91326363e",
"databaseURL": "https://fir-blog-9304f.firebaseio.com",
"storageBucket": "fir-blog-9304f.appspot.com",
"locationId": "europe-west",
"apiKey": "AIzaSyDxTIw-McJJ-pd1rqYCryeKWwkUaTzutlQ",
"authDomain": "fir-blog-9304f.firebaseapp.com",
"messagingSenderId": "388751691931"
});

Make it the first import in our React application entry point — webapp/src/index.tsx. This way, we ensure we won’t be interacting with Firebase before it’s properly initialized.

import "./firebase";
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(<App/>, document.getElementById('root'));serviceWorker.unregister();

From now on, you can call its APIs freely from anywhere in your code.

Persistence with Firestore

The first service we’ll use (besides Hosting) is Firestore. It’s a really neat document database platform. One of its selling points are real-time updates. It makes it really easy to get notifications on any data changes, wherever they originate from.

Let’s get started. First, we need to enable the web console. When prompted, select “Start in test mode” — we’ll get back to access control in a moment:

There’s a simple database browser available, though there’s no data to show yet.

Now, let’s integrate our web app with Firestore. Create a new file webapp/src/Todo.tsx and implement a simple list:

import React, {FormEvent, useEffect, useState} from "react";
import "firebase/firestore";
import firebase from "firebase/app";
interface Todo {
id: string;
name: string;
}
const firestore = firebase.firestore();export function Todos(props: { userId: string }) {
const {userId} = props;
const [items, setItems] = useState<Todo[]>([]);
useEffect(() => {
return firestore.collection(userId)
.orderBy('name')
.onSnapshot(snapshot => {
setItems(snapshot.docs.map(doc => ({
id: doc.id,
name: doc.data().name,
})));
})

},
[userId]
);
function add(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
const form = e.target as any;
const name = form.elements.name.value;
form.reset();
firestore.collection(userId).add({name});
}
return <div>
<form onSubmit={add}>
<input name="name"/>
</form>
<ul>
{items.map(item =>
<li key={item.id}
onClick={() =>
firestore.collection(userId).doc(item.id).delete()
}>
{item.name}
</li>
)}
</ul>
</div>
}

Firestore deserves an entirely separate blog post. I’ll try to distill it into one paragraph.

In Firestore, you can store JSON documents, which are then organized in collections. You can add documents using collection(name).add(doc) and delete them with collection(name).doc(docId).delete(). To retrieve documents, you can just query for them, but there’s also a more robust way. This is where Firestore really shines. You can ask it to notify your app whenever documents matching a specific query change. In this case, we’re subscribing to changes on a whole collection using collection(name).onSnapshot(callback). As you see, it’s as easy as fetching JSON from a REST endpoint.

In <Todos> component, we’re using userId prop as the collection ID. This is a common pattern and will greatly simplify things later on. Before we enable proper authentication, just use 'guest' for now.

Update webapp/src/App.tsx to start using this component:

import React from 'react';
import './App.css';
import {Todos} from "./Todos";
function App() {
return <div>
<Todos userId='guest'/>
</div>;
}
export default App;

Check it out locally in React’s dev mode:

npm --prefix webapp start

Open a few browser windows pointing to http://localhost:3000 and marvel at the real-time synchronization we got for free:

Authentication

Right now, everybody accessing the app shares the same “to-do” list (as a guest). Let’s change that. The first thing to do, is to require users to log in. Firebase supports several sign-in methods including Google, Facebook, Twitter, Github, Microsoft and Apple. Simple registration with email and password is also an option. For the least hassle, we’ll be asking users to authenticate with a Google Account.

First enable the Google Authentication provider in the web console:

The rest of the configuration can be done via the command line.

We’ll create a login button, which will open Google Sign-In in a popup. The same button will allow logging out, if the user is already authenticated.

Create webapp/src/AuthButton.tsx containing the code below:

import firebase from "firebase/app";
import "firebase/auth";
import React, {useEffect, useState} from "react";
const auth = firebase.auth();export function useUser() {
const [user, setUser] = useState<firebase.User | null>();
useEffect(() => auth.onAuthStateChanged(setUser), []);
return user;
}
export function AuthButton() {
const user = useUser();
if (!user) {
const provider = new firebase.auth.GoogleAuthProvider();
return <button onClick={() => auth.signInWithPopup(provider)}>
Login
</button>
} else {
return <button onClick={() => auth.signOut()}>
Logout {user.displayName}
</button>
}
}

Note the useUser() React hook. Its purpose is to glue together React’s useState and Firebase’s onAuthStateChanged.

If you don’t want popups, you can call auth.signInWithRedirect(…) instead. Lastly, let’s use the new button. Update webapp/src/App.tsx:

import React from 'react';
import './App.css';
import {Todos} from "./Todos";
import {AuthButton, useUser} from "./AuthButton";

function App() {
const user = useUser();
return <div>
<AuthButton/>
{user && <Todos userId={user.uid}/>}
</div>;
}

export default App;

That’s it! We have a working Google Sign-In integration.

Now every user has their own collection of To-Dos. But still, there are no access restrictions — everybody can read and write to everybody’s collections. You can see it for yourself by adding <Todos userId="guest"/> — it will work regardless of whether you’re logged in. Let’s change that.

Firestore security rules

In order to restrict access to specific documents, Firestore introduces the concept of Security Rules.

There’s a rules editor in the web console. It has basic code completion, letting you discover the available context. It’s the best place to get started with rules.

Here are the rules to allow access only to collections with names matching the current users identifier:

rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{uid}/{document=**} {
allow read,write: if request.auth.uid == uid;
}
}
}

Writing rules in the web editor is convenient, but it’s better to eventually store them with the application’s source code in version control. To do this, run firebase init again and select:

? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to confirm your choices. 
◯ Database: Deploy Firebase Realtime Database Rules
❯◉ Firestore: Deploy rules and create indexes for Firestore
◯ Functions: Configure and deploy Cloud Functions
◯ Hosting: Configure and deploy Firebase Hosting sites
◯ Storage: Deploy Cloud Storage security rules
◯ Emulators: Set up local emulators for Firebase features

This will store the rules in firestore.rules (and also firestore.indexes.json). From now on, rules defined in this file will be used after every firebase deploy.

At the beginning of this post I promised one more thing — server side code. To continue, go to the second post in this series:

--

--