The new ease of building app-like, reliable and engaging web apps
or: Using Angular and Cloud Firestore to create a basic PWA in less than 1 hour
Google just announced Cloud Firestore as their next flagship database for Firebase. In the last months I had the opportunity to test Cloud Firestore and have been able to try it in Angular Projects.
While Firebase’s RTDB has its use cases, a lot of developers have been missing a more traditional “real” database. Firestore introduces a lot of features, providing it with a queryable Database that automatically scales on a great level and offers multi-region data replication.
If you want to dive deeper into Firestore’s features the official announcement is a good starting point:
Recently I spent a lot of time working on modern Browser Apps, predominantly using Angular. Not long ago it was quite some work to create your own build process to deploy your Angular app including a service worker.
Over the last months the Angular CLI has gotten a lot of love regarding service workers and today it almost handles the complete build on its own for a lot of use cases.
Recently I read a comment in a blog post which I deemed to be very fitting:
“Using angular is like hiring google engineers to code for u for free”
While Angular makes it easy to serve your app and it’s assets via a service worker, you had to build your own offline solution for your database connection.
Firestore now offers offline persistence in it’s Web SDK out of the box. You can find further information on Firestore’s persistence (and it’s limits) in the official docs:
Combining Firestore and Angular is a great to build modern web apps. It offers you a Toolbox to create reliable and app-like applications and gives you the power to create an offline enabled web app in no time.
Demo App
In a demo App I would like to share how fast and easy it is to build a PWA with those tools. For demonstration purposes we will build a Craft Beer Knowledge PWA. Different brands of beers will be shown and users can filter and sort this view. All queries will be handled by Cloud Firestore on “server-side”.
Users should be able to add and edit information about different ales and beer brands.
To get started even faster I will be using Angular/material Components and the Angularfire2 library to use Datatypes.
After creating a new Angular app with the CLI we need to install all dependencies for Angular/Material, Firebase and Angularfire2.
First we need to import the AngularFireModule into our app.module.ts and call initializeApp with the config of our newly created Firebase app. We also need to import
AngularFirestoreModule.enablePersistence()
to enable Firestore’s persistance.
Lets get started
Data Structure
Firestore’s Data Model let’s us create collections of documents. Those documents can contain collections and fields of different types. For our app we will be organizing beer-documents in one collection of beers. Each beer document will contain several fields.
With Angularfire we can access our collection by calling AngularFirestore.collection(‘collectionPath’)
Writing Data
To add a new document we simply call add(doc). This will return a Promise.
Since Promises can only emit a resolve or reject once, we won’t be able to track different events (like submitted or written to DB) of our write request. It will only resolve when our new document has been written to Firestore or will be rejected on an error.
That’s why our app logic can’t wait for the promise to resolve if we want to be able to do offline writes. We won’t be returning this Promise to our app logic and instead we will use a openTransactions variable to keep track of our Unwritten Data. Every time we call a write we add a transaction and when the Promises resolves we remove it. We will be using this variable to give feedback to a user.
Reading Data
Time to read from Firestore for the first time: Angularfire offers different options to access your database. The easiest way to access data is to call collection(‘path’).valueChanges() to get an Observable of our data as an array. This is kind of similar to requesting RTDB’s FirebaseListObservable. We can simply subscribe to it and render all our data into a list.
For our app we will subscribe to snapshotChanges() which will allow us to access some additional metadata and the ids of our documents. Using this we will be able to use our document ids to update data later on. As we subscribe to this method all changes will be synchronized with our client automatically and Angular will rerender our list.
The included metadata is useful to determine whether the received data is coming from the cache or is actual “live” data.
Query Data
Both of those “read” methods on collection() offer an optional callback to run queries on our data. This is where the magic happens. I’m using this to order and filter our beers.
It is possible to use far more complex queries, but there are some limitations.
In my experience those limitations especially occur when you try to query on nested data. Like in good old RTDB days, there is a “Firebase Way” (mostly flatten your data) of getting around most of these limitations. It is kind of important to know how your queries will look like when you design your data model. This is of course not Firebase exclusive but even more important using Firebase.
One thing I really miss is a build-in way to perform a full text search on strings. For now we will have to use a 3rd party service like Algolia. However, the Firebase docs do provide an example on how to use Firebase Functions to index your data with a service like Algolia.
Offline Persistance
As mentioned Firestore handles offline persistence out of the box. It will cache all reads in an IndexedDB and we are able to reread this cached data and even run different queries on it.
While our writes Promises do not resolve while we’re offline, the “requested” changes will influence our local data immediately.
Authentication & User Rights
To make sure only registered users are able to add beers to our database I used Firebase authentication to set up user auth. I implemented a simple login and signup form. Once a user is signed in he will be able to add new data.
By adding a createdBy value to each beer object we will be able to keep track of who added which beer. We will set this value to our Firebase auth uid.
That enables us to use it on the client side and allow users to edit all data they added to the database.
In our Firestore security rules we will use this value to make sure user can only alter data they did publish.
Security rules for Firestore are not the same as in RTDB, they are more similar to those you use for Cloud/Firebase Storage. For me they feel less intuitive and more complicated.
It takes up to 10 minutes till all requests are affected by new rules, this does not help to make things easier. It could help if there was a test environment available (like the one on RTDB) to test our rules in the Firebase Console. (actually there is a workaround using the Firebase Rules API, but this is far less comfortable than the mentioned simulator)
A possible solution for our demo app could be a rule like this:
Merge Rules
Since users could be offline for a long time before their writes are submitted to our database, we have to think about what happens if the same data has been altered from another device in the meantime. If we want to prevent this, one solution could be to add a versioning value to our data. We could add a security rule in our Firebase console, that will only allow overrides of lower versions.
Build
To get our app ready for deployment we want to use the Angular-CLI with all of its automated optimizations.
Service worker
To enable offline support we need to deploy our web app with a service worker. Fortunately the Angular Team did also prepare the CLI to help us with that.
(if you are interested in creating service workers I recommend to have a look at http://serviceworke.rs )
For now I will use the Angular-CLI to handle the creation of our service worker. In order to do so, we need to install the @angular/service-worker package and tell the CLI by calling ‘ng set apps.0.serviceWorker=true’ that we want to build with a service worker.
The CLI will now generate our service worker and a ngsw-manifest.json file automatically. This file serves as a configuration for the service-worker. Among other options it contains a list of all our static assets that should be cached.
While this part is automatically generated by the CLI, there are some things we want to set up. As we are using external resources we need to make sure they are also cached. To do this we include an “external” object in our file.
This will make sure that e.g. our fonts are available when we’re offline. In this file we can also configure our service worker for web push notifications and inform the service worker about our app routes. For this example we will simply redirect all requests from the root route to index.html.
Since the ngsw-manifest file is recreated on each build, we would have to add these changes after every time we create a new build. If we save those manual edits to a ngsw-manifest.json in our project root, the CLI will merge it into the created manifest on every build.
Push notifications for web apps
Another really great feature of service workers is that they enable your app to send push notifications to a user’s device. You can also set up your service worker to handle push notifications in the ngsw-manifest.json file. The @angular/service-worker package also provides you with the tools you need to handle push notifications.
Further reading
The service-worker package for angular is in an experimental state and there might be changes. It is possible that it will be stable in version 5. There is a lot more to learn about service workers, if you are interested in service-workers with the Angular CLI, take a look at this document:
Progressive Web Apps using the Angular Service Worker
Manifest
One missing part for an app-like experience is to set up a manifest.json for our web app. This config file defines the appearance of our app in browsers and operating systems.
For a production app you should invest some effort to optimise all icons and configurations for different browsers and mobile platforms. For this demo I simply took some logo PNG file and used realfavicongenerator.net to generate a manifest, the needed icons and some additional config files. This tool even gives us the needed import code for our index.html.
Copy and paste all assets and imports and now our app is ready to be added to some homescreens.
Deploy
With all of this set up we can simply start the actual build with:
ng build — prod
Now the Angular-CLI will do all the work needed and our PWA is ready to be deploy with:
firebase deploy
Conclusion
Being a queryable database, Firestore feels like the missing part in the Firebase universe, making it a potential solution for more use cases. Compared to the good old Realtime Database it is a big step forward for any non chat app, providing scaling queries without giving up the real time updates.
Nevertheless, there are some features that we are used to have in RTDB (e.g. a simulator for security rules and an easy/automated way to backup and import/export your data), which are not (yet?) available in Cloud Firestore.
To decide if Firestore does fit your project you will have to dive deeper into its data structure and query limitations.
If you would like to know more about Firestore I recommend you to take look at the docs. The docs are detailed and provide solutions for common problems.
If Firestore does fit your project, the built-in offline persistence is a great feature to further speed up your app development. In combination with Angular, it feels like we finally don’t have to worry about infrastructure and can just focus on building awesome apps.
Progressive Web Apps are evolving a lot, there are more and more great browser API’s coming and in the near future all major browsers will support service workers. With that in mind, it is great to have a toolset that allows us to turn single page applications into “real” apps that will work on all platforms.
Note:
I decided to go for a demo about craft beer because I think they have been an enrichment to our drinking culture in the last years. I also didn’t want to create another to-do-list or chat app. However, I am by no means promoting harmful drinking manners and fully support the concept of responsible drinking.
Demo App: https://beer-pwa.firebaseapp.com/