DEV Community

MartinJ
MartinJ

Posted on • Updated on

2.2 A student's guide to Firebase V9 - Coding a simple webapp

Last Reviewed: Aug 2023

Introduction

A previous post, (Firebase V9. Part1 - project configuration), described the steps you need to follow to get yourself to the point where you could start coding. Here, at last is your chance to write some javascript - you've certainly earned it!.

If you've read my initial "An Introduction to Google's Firebase" post, you may have written some code already. Remember this?

<p id="test">Hello there</p>
<script>
let hourOfDay = (new Date()).getHours(); // 0-23
if (hourOfDay < 11) {
    document.getElementById('test').style.color = "blue";
} else {
    document.getElementById('test').style.color = "red";
}
</script>
Enter fullscreen mode Exit fullscreen mode

Copy this over the content of index.html in the public folder, rerun the deploy command and refresh the https://fir-expts-app.web.app tab - the screen should now display "hello" in an appropriate colour, depending on the time of day.

Yes, it's that easy! But don't get over-confident - there's a lot still to learn!

What I'm going to do now is introduce you immediately to the Firebase functions that read and write data from a Firestore database. The example I've chosen is a simple "CRUD" (create, read, update and delete) application that will show you the basics. It's a good old-fashioned "shopping list" maintenance script.

When the shopping list webapp runs it's going to display a screen along the following lines:

Shopping List screen

I know this isn't going to win any trophies for quality user-interface design, but please bear with me - I'm trying to keep things as simple as possible so that we can concentrate on the Firebase issues. However, if you were to give my code a try, you'd find that it does work. User ab@gmail.com could run this script to pull down a current copy of their shopping list, insert a new item with the "Create Item" button, amend the specification of this with its Update button and remove it with its Delete button.

The way I'm approaching the design for this webapp is to use an index.html file to lay out a skeleton for this screen. Here's the <body> code.

<body style="text-align: center;">

    <h2>Shoppinglist for :
        <span id="useremail"></span>
    </h2><br>

    <div>

        <!-- [userPurchase] [update button] [delete button] to be added dynamically here-->

        <span id="usershoppinglist"></span><br><br>
        <input type='text' maxlength='30' size='20' id='newpurchaseitem' autocomplete='off' placeholder='' value=''>
        <button id="createitembutton">Create Item</button>
    </div>

    <script type="module" src="index.js"></script>

</body>
Enter fullscreen mode Exit fullscreen mode

You'll notice immediately that quite a few things are missing from this code. For a start, there's nothing in the code for the Shopping list for : header identifying the owner of the list - just an empty <span> with a useremail id. Likewise the content of the shopping list block is identified but not specified. How is this ever going to work?

The information we need here exists in a Firestore database but can only be displayed when we retrieve it. So, we're going to make this work by adding some logic to our system - a bunch of javascript code that can be started up when the html file is loaded and that will perform the necessary database access tasks as required. Once the code has done its job, we can use the techniques that were first introduced in the "Jungle" post to "insert" the missing html into the screen skeleton.

You might wonder, if I'm generating html code in javascript, why I bother with the html skeleton at all - why not just generate everything inside the <body> tags? The answer is that the skeleton is a great way of documenting the "structure" for your code. When html is generated inside javascript you'll find its clarity is seriously compromised and you start to lose track of the overall design. By contrast, when the design is defined in raw html, neatly indented and highlighted by the code-formatting tools in your IDE, it's much easier to see what's going on. I find it helpful to add "code comments" too, documenting the intended structure for any "missing" bits

Another difference between the code I'm showing you now and the examples I've used so far is that I'm no longer coding the javascript directly inside the html file. Instead, there's a <script> entry that simply refers the browser to an independent index.js file. This paves the way for use of special performance features introduced by Firebase 9. Note that the type of the script is declared to be module - more on this shortly.

In passing, I'd just like to mention that this style of development, wherein html code is dynamically-generated by javascript code, is the hallmark of "single-page app" architectures, a term first introduced above in the context of firebase initialisation using the CLI. In the past it would be common for an application to present its users with an array of options laid out as tabs at the top of a screen display. The usual practice was to develop the code associated with each tab as a separate html file. The tabs would then be implemented as buttons each specifying an onclick referencing the file that was to be opened. This arrangement made life complicated for the developer, however, and the use of javascript in the application has opened up the opportunity to keep the code together in a "single-page app". See What is a single-page app for further background.

Simple Firebase CRUD code

As you've seen, when the shopping list webapp runs, the first thing that it needs to do is to display the current shopping list content. I've said that we're going to get this from a Firestore database so it's time for you to see what one of these looks like. In this section we're going to start by creating a database.

The data structure I have in mind for this application might go something like the following:

Test data structure

Here, the "shopping list" data just consists of pairs of email addresses and purchase items. The idea is that the system should permit many different users to share the same database - the email fields will allow us to keep their shopping lists separate. If things take off, perhaps we'll have millions of users!

In Cloud Firestore's NoSQL data model, data is stored in "documents" that contain fields mapping to values. These documents in turn are stored in "collections". A database thus consists of a set of collections inside each of which data is stored in documents.

The modelling of data structures and the design of databases to hold them is an extremely important aspect of system design, well beyond the scope of this simple introduction. Suffice to say that the facilities provided by Google within the Firestore framework are a sophisticated response to the requirements of modern IT applications. You might find it useful to use the web to read around the subject - Why successful enterprises rely on NoSQL might be a good place to start.

One important element of data modelling is the identification of "keys" - data fields that can be used (generally in combination) to uniquely identify documents. Often there's a natural key - for example "city name" in a collection of documents describing the characteristics of individual cities. Annoyingly, in our userShoppingLists collection, there isn't a natural key - but this is quite commonly the case and so you'll not be too surprised to find that Firestore is happy to generate artificial keys automatically in this situation.

Actually, I've chosen this example precisely because its documents don't have a natural key (much of Google's Firestore documentation describes cases where a single field provides a natural key - something that in my experience is really quite unusual) and so my example pushes Firestore a bit harder. Firestore code for the two cases (natural key v generated key) is slightly different, the generated key form being a bit more complicated. But the advantage of using automatically-generated keys is that this approach can be used in all situations and so your code can follow a single style.

It's time now to go back to the Firebase console for our webapp project. Select the "Firestore Database" tool from the column on the left and proceed to initialise the database.

After a certain amount of preamble during which you specify a starting mode for security rules (select test for now - we'll put things on a production level later) and select a geographical location for the google servers that will hold your data (for UK users, anything starting with eu will be fine for a test development). Click "done" to "provision" your database and reveal the Firestore "collections management page" for the project.

It has to be said that the "management page" is a seriously tedious way of entering test data, but the screen works pretty well for the basic task of specifying and structuring collections in the first place. I don't think I can significantly improve on Google's documentation for this procedure, so I'll simply refer you to Managing Firestore with the console at this point. Try to create a collection called userShoppingLists for the data shown above. Remember that I have said that documents in the userShoppingLists collection should use automatically-generated keys. You should end up with something like the following:

Firestore Collections Management Screen

Those curious code in the userShoppingLists column are the automatically-generated keys for individual shopping list entries.

Right, with all this preamble concluded, let's concentrate on the application logic and the Firebase code located in the index.js file. Here it is:

// see https://firebase.google.com/docs/web/setup for latest browser modules source ref

import { initializeApp } from 'https://www.gstatic.com/firebasejs/9.4.0/firebase-app.js';
import {
    getFirestore, collection, query,
    getDocs, where, orderBy, setDoc, doc,
    deleteDoc
} from 'https://www.gstatic.com/firebasejs/9.4.0/firebase-firestore.js';

const firebaseConfig = {
    apiKey: "AIzaSyAPJ44X28c .... 6FnKK5vQje6qM",       //"obfuscated" code - see below
    authDomain: "fir-expts-app.firebaseapp.com",
    projectId: "fir-expts-app",
    storageBucket: "fir-expts-app.appspot.com",
    messagingSenderId: "1070731254062",
    appId: "1:1070731254062 ... b61bd95caeacdbc2bf",    //"obfuscated" code - see below
    measurementId: "G-Q87QDR1F9T"
};
const firebaseApp = initializeApp(firebaseConfig);
const db = getFirestore(firebaseApp);

const email = "ab@gmail.com";

window.onload = function () {

    document.getElementById('useremail').innerHTML = email;
    document.getElementById('createitembutton').onclick = function () { createShoppingListDocument() };

    displayShoppingList(email);

}

async function displayShoppingList(email) {
    // retrieve the shoppingList documents for email and turn them into entries 
    // in an editable Shopping List table

    let userShoppingList = "";
    const userShoppingListsCollection = collection(db, 'userShoppingLists');
    const userShoppingListsQuery = query(userShoppingListsCollection,
        where("userEmail", "==", email), orderBy("userPurchase", "asc"));
    const userShoppingListsSnapshot = await getDocs(userShoppingListsQuery);

    userShoppingListsSnapshot.forEach(function (doc) {
        userShoppingList += `
        <input type='text' maxlength='30' size='20' id='o` + doc.id + `' autocomplete='off'
            placeholder='` + doc.data().userPurchase + `'
            value='` + doc.data().userPurchase + `'>
            <button id =  'e` + doc.id + `'>Update</button>
            <button id =  'd` + doc.id + `'>Delete</button><br>
            `;
    });

    document.getElementById('usershoppinglist').innerHTML = userShoppingList;
    userShoppingListsSnapshot.forEach(function (doc) {
        document.getElementById('e' + doc.id).onclick = function () { updateShoppingListDocument(doc.id) };
        document.getElementById('d' + doc.id).onclick = function () { deleteShoppingListDocument(doc.id) };
    });

}

async function updateShoppingListDocument(id) {
    // update the userPurchase field for document id

    let newUserPurchase = document.getElementById("o" + id).value
    const docRef = doc(db, 'userShoppingLists', id);
    await setDoc(docRef, { "userPurchase": newUserPurchase }, { merge: true });
}

async function deleteShoppingListDocument(id) {
    // delete the document for document id

    const docRef = doc(db, 'userShoppingLists', id);
    await deleteDoc(docRef);
    displayShoppingList(email);
}

async function createShoppingListDocument() {
    // create a new document, leaving Firestore to allocate its document id automatically

    let newUserPurchase = document.getElementById("newpurchaseitem").value;
    const collRef = collection(db, "userShoppingLists");
    const docRef = doc(collRef);
    await setDoc(docRef, {
        "userEmail": email,
        "userPurchase": newUserPurchase
    });

    displayShoppingList(email);
    document.getElementById("newpurchaseitem").value = '';
}
Enter fullscreen mode Exit fullscreen mode

The script starts with a bunch of import statements. Firebase 9 delivers its library code to the application via "modules", one for each major function group (eg "authentication"). When we import one of these, we must also declare the component functions that we want to use - the aim being to minimize the size of the application.

One consequence of using module import statements in a script is that a javascript file that contains them itself becomes a module - more on this later.

Because in this post I want to concentrate on the essentials of Firestore coding, I've chosen to use what Google chooses to call the "browser module" form of its Firebase libraries (see Getting started with Firebase for the web at 5.15). These are .js files with an https:// address drawn down at run time from the web. In a production application, you'd use modules that you first install in your terminal environment with npm and which you "package" into your javascript using a tool like "webpack" prior to deployment. This is more efficient, but since efficiency isn't an issue just now and deploying your project when you use "proper" modules adds complications (because browsers don't understand these without further attention) I've chosen to avoid this complication just now. So, "browser modules" it is.

Immediately after the import statements we get our first sight of a firebase function in action - an initializeApp() call that will give our webapp (running in our browser) a db object linking it to our database (sitting out on the web in the Google cloud). This link is delivered with reference to a firebaseConfig json supplying all the necessary keys (see Eloquent Javascript for a description of the json format). The contents of this json were defined when we created our Firebase project and can be found by opening the Firebase console for the project and clicking the gear wheel icon to view the project properties. I got these into my index.js file by simply copying and pasting.

You'll have noticed that a couple of the items included in my config json listing have been disguised. They look like security keys and, indeed, that's exactly what they are. Possession of these keys takes a potential hacker one step closer to getting into my database.

Since you now know enough about "inspecting" Javascript code in a browser you'll realise that the codes will be visible when I deploy my application (which will, of course, contain the undisguised keys). So how do I keep the hackers out? Later in this post I'll describe how you add a login layer to engage Firebase's essential security mechanism - Firestore collection-level "rules". With these in place, knowledge of the keys alone won't be enough to gain access.

So why do I bother to disguise the keys above at all? It's just a practical issue. If I put posts like this onto the web with real keys inside them I will sometimes receive stomach-tightening messages from the recipient systems telling me that I've just published a security key - did I mean to? I don't want to get into a habit of automatically ignoring these, so it's best to short-circuit the issue by turning my codes into something that doesn't look like a security key in the first place. Plus, of course, there's no sense in creating unnecessary advertising!

Once the webapp has successfully created its db object, it's free to do anything it likes with this database. We'll talk about the security implications of this later, but for now let's just concentrate on applying this new-found freedom and using it to read a shopping list!

If you scan down the remainder of the code you'll see it consists largely of four functions, one for each of the four CRUD operations. The first thing to note is how compact the code is. For example, the deleteShoppingListDocument(id) function used to delete a document with id id from the userShoppingLists collection is just three lines long (and one of those is not strictly anything to do with the deletion process because it simply refreshes the screen to confirm the successful completion of the deletion operation). This, I suggest, is a modern miracle - in the past, such functions would have used as a whole bunch of complicated javascript calling an equally sophisticated piece of PHP code (or similar host-based language) stored in a separate file and hosted on a separate device.

Sticking with the deleteShoppingListDocument(id) function, note that the core of this is a call to a deleteDoc() function preceded by an await keyword (an extension added to the javascript language only relatively recently). My "Jungle" post describes the "asynchronous" nature of all javascript calls to file IO (input/output) functions. This is an example. In normal circumstances, a deleteDoc() call will certainly initiate the necessary deletion action, but control flow in the program making the call will pass immediately to the next statement - ie, without waiting for the deleteDoc() result. In the present case, unless we take some special precautions, the displayShoppingList(email) in the next statement might well simply show an unchanged display (because the deletion hasn't taken place yet)

However, in the case of this particular piece of code, we've used the await keyword. As a result, control doesn't reach the screen refresh call until the deleteDoc() has finished. Note that a call to deleteShoppingListDocument() itself won't wait for a result though. You still need to keep your wits about you when you're working with asynchronous operations!

Note also that in order to use the await keyword we have had to declare the parent deleteShoppingListDocument(id) function as asynch.

I'm not going to go into detail here about the precise form of the individual Firestore functions used to perform the CRUD operations - I think you've probably got more important things to worry about just now. But when you're ready, you might find the cheatsheet at 10.1 Reference - Firestore CRUD command templates for Web Version 9 a good point to start. This contains links to Google's own documentation if you want more details. Meanwhile, there's one wrinkle that I do want to mention.

If you look at the code for the createitembutton button in the index.html file, you'll see that it doesn't specify what's to happen when the button is clicked. Normally I'd have done this by including an onclick = clause to direct the button to the appropriate CRUD function. While this is an arrangement you might have used freely in the past with "ordinary" scripts, I'm afraid that we have to do things differently when we're using modular scripts.

In this case, if you tried the conventional approach, when you clicked the button you'd find that your program would tell you that "your onclick function is undefined". What? But it's there - in the script!

Well it might be in the script, but the script is declared as type module (it has to be in order to enable us to use the import keyword to load our Firebase api functions) and the "namespace" for a module (ie the collection of variable and function names referenced in the script) are only available to that module. In particular, they're not available to the DOM. This arrangement is designed to ensure that modules don't interfere with each other (ie so they're 'modular').

What we have to do is add the onclick to the button dynamically in the module once the DOM has loaded. So if you back at the code for index.js you'll see that one of its first actions is to launch the following statement:

document.getElementById('createitembutton').onclick = function () { createShoppingListDocument() };
Enter fullscreen mode Exit fullscreen mode

This completes the setup of the button and allows us to use it in the DOM.

You may be pleased to hear that all of this nonsense gets properly sorted out in 2.3 A student's guide to Firebase V9 - A very gentle introduction to React.js, when you start to use React to build the bridge between your Javascript code and the browser's DOM.

Things get a little more complicated in the displayShoppingList() function where we dynamically generate html to display complete buttons alongside the <input> items on which they are to act (and note, in passing, how confused the html code specification is here - perhaps you'll see now why I was concerned to use the index.html file to define the layout aspect of the webapp). In this case you might think we could generate a button complete with its onclick specification all at the same time. But if you tried this, having inserted the code block into the DOM with the

document.getElementById('usershoppinglist').innerHTML = userShoppingList;
Enter fullscreen mode Exit fullscreen mode

instruction, you'd find that your new buttons failed in exactly the same way as previously described. What we have to do is first generate the code without the onclick specification, update the DOM and then add the onclicks. This explains the second

    userShoppingListsSnapshot.forEach(function(doc) {
Enter fullscreen mode Exit fullscreen mode

loop in the displayShoppingList() function's code.

This is a nuisance, (entirely consequent on Firebase Version 9's move to a modular approach) but a small price to pay for the gains one obtains elsewhere through the use of the Firebase api.

Now that I've homed in on the forEach structure, I think I should also say a bit about this too. "Queries" are used to get "snapshot" subsets of the documents in a collection in response to a specification of selection and sorting criteria. They're documented at Querying and filtering data .

Once you've got a snapshot, the foreach construct allows you to work your way through all the documents that it contains. For each doc, you have access to both its data items (as doc.data()."item name") as well as the document id itself (as doc.id). In this particular instance I use the document id as a convenient way of applying an identifier to the <input> and <button> elements and supplying parameters to their onclick functions.

Something else you should know about queries is that they will almost always need to be supported by an index (ie a quick way for Firestore to check which documents match selection criteria without reading them the whole collection). The data tab in the Firestore Database tool gives you a method of creating indexes, but you might actually find it easier just to let your queries fail and pick up the consequences in the browser system tool. This is because the error announcing such a failure will include a helpful link that, when clicked, will create the index for you. This is a seriously useful arrangement. Thank you Google!

In summary, there are quite a few other "wrinkles" to using firestore functions on complex data structures, but overall, I'll think you'll find that everything works pretty smoothly. My own experience has been overwhelmingly positive - a huge improvement over the technologies I've used previously.

Important caveat

I've saved writing the next few paragraphs till now because I didn't want to distract you from the main task of getting your head around Firebase. But if you're a real beginner and have never seen the problem I'm about to describe, there's a "feature" of browser-based development which may really perplex you. The symptoms are these: you've changed something in your index.js, redeployed to the Cloud and when you run your webapp - roll of drums - nothing has changed. What the..!!!!!!?? Take a deep breath. What's going on here is that the browser is trying to help your users. Loading your index.js from a script puts a strain on everything and so the browser figures "why not just keep a copy of this in my local cache file and serve it from there?". This means the responsibility for telling the browser that the file has changed is down to you! Great for the Internet but a real pain for you as a developer. And oh, by the way, what I've just said also applies to image files etc in your Assets folder.

Just how are you meant to cope with this? There are actually several ways and the good new is that one of them is pretty much painless. Where the problem bites hardest is when you're actively developing and debugging code and here you can take advantage of a feature of the browser's system tools code inspector itself. If you click the network tab here you'll find you can set a flag to instruct the browser to ignore its cache. This mean that if you reload your webapp while the inspection tool is open, your index.js file (and everything else) will be refreshed from the Cloud copies. Phew. Here's a picture of the magic checkbox - my advice is to just leave this permanently checked.
Inspection tool cache refresh checkbox

When you're in production mode, however, matters aren't so easily fixed - obviously you can't tell your users "the version's changed, please open the inspection tool"!. Here there's no alternative but to "change the name of the file". But this is obviously seriously inconvenient for you, the developer. Fortunately, there's a trick that we can pull here. To a browser, "name of the file", isn't in fact just the filename itself but includes any parameters that might be attached to it. You'll have seen "parameters" yourself lots of times - they're the funny sequences of "?"s and "+"s etc that appear in a browser's url line when you are doing a search. So. if you want to make your browser think that index.js has changed, all you need to do is change the<script> tag referencing it to something like:

    <script type="module" src="index.js?ver=1.2"></script>
Enter fullscreen mode Exit fullscreen mode

However, when it comes to Asset references, where you may have innumerable changes of this type to make, something more systematic will be needed. But you'll know enough now about the flexibility of Javascript to realise that it's probably possible to arrange things so that all the consequential HTML you'll need can be generated with reference to this one, versioned, src= filename. I leave this as an exercise for you, the reader.

Adding a login to secure the database from unauthorised access

But we can't relax just yet. There's still a large hole in the functionality of this webapp because, when we initially configured our database, we created it as a "test" deployment. Currently we're connecting to our firestore database by referencing our firebaseConfig data item with all its apikeys etc. As described earlier, anybody skilled in the use of browser tools will be able to read this from the webapp. Then, as things stand at present, there's nothing to stop them copying this into their own webapp and gaining access to our database.

Rather than trying to hide the firebaseConfig item (a fruitless task), Google provides a cloud-based arrangement, stored within our Firebase project and thus accessible only to us via our Google account. This allows us to specify the tasks (read, write etc) that can be performed against specified criteria (eg "user logged into our project"). What I mean by "logged in" in this instance means "having presented a user id and password that matches the settings for a user known to our Firebase project". So, it's time to look at adding a login function to our webapp.

The Firebase arrangements for protecting our database are defined using "rules" that we define using a simple coding system in the Firebase Console for our project.

If we select the Firestore Database tool on the console and click the rules tab, we'll see the current rule specification. At this stage this will still be set to the initial "test" state and will look something like the following:

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
     allow read, write: if true;
    }  
  }
}
Enter fullscreen mode Exit fullscreen mode

This is basically saying "allow everybody both read and write access to everything". Only firestore apis are permitted to access firestore cloud data and every firestore api call (eg, deleteDoc()) asked to perform a read or write operation on a document will first inspect the project's rules to see whether or not that proposed action is permitted. While our rules remain as above, the api calls will allow everything.

In our case, we want to arrange things so that documents can only be read and written by "logged-in" users. The rule specification therefore needs to be changed to :

service cloud.firestore {
  match /databases/{database}/documents {

    match /userShoppingLists/{document} {
        allow read, write : if request.auth != null;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

See Google's documentation at Basic Security Rules for a description of the rules-specification language. This is a wonderfully powerful and flexible arrangement, though it has to be said that the language can be difficult to work with. Fortunately the specification tab comes equipped with a "playground" that allows you to check out the validity of your rules before you publish them (ie, apply them to your live database).

So far so good. Once your rules are updated as indicated above published, you'll find that your app won't work any more. If you "inspect" the code in the browser, you'll see that your database access commands are being rejected with "insufficient privilege" messages. THe problem of course is that the rules have now been set to allow database access only to users who are "logged in". How do you get your users (and yourself) "logged-in"?

The short answer is "by using one of the methods that Firebase provides to log them in".

Quite the easiest way to achieve this (since we're using Google services ourselves) is to accept users as logged in if they're logged in with Google. To do this, take the following steps:

(a) Add a "login" button to the index.html file (we can't just launch the Google login popup automatically because in some circumstances this will be blocked - it must be initiated by explicit user action):

<body style="text-align: center;">
    <button id="loginbutton">Login</button>

    <div id="shoppinglistpage" style="display: none;">
        <h2>Shopping list for :
            <span id="useremail"></span>
        </h2><br>
        <div>
            <!-- [userPurchase] [update button] [delete button] to be added dynamically here-->
            <span id="usershoppinglist"></span><br><br>
            <input type='text' maxlength='30' size='20' id='newpurchaseitem' autocomplete='off' placeholder='' value=''>
            <button id="createitembutton">Create Item</button>
        </div>
    </div>

    <script type="module" src="index.js"></script>

</body>

Enter fullscreen mode Exit fullscreen mode

(b) add a new import statement at the top of the index.js code to draw in the new GoogleAuthProvider, signInWithPopup functions we're going to reference:

import { getAuth, GoogleAuthProvider, signInWithPopup } from 'https://www.gstatic.com/firebasejs/9.4.0/firebase-auth.js';
Enter fullscreen mode Exit fullscreen mode

(c) replace the temporary email = "ab@gmail.com"; "fudge" and the window.onload function with the following code:

var provider;
var auth;
var credential;
var token;

var email;

window.onload = function () {
    document.getElementById('loginbutton').onclick = function () { signIn() };
    document.getElementById('createitembutton').onclick = function () { createShoppingListDocument() };
}

async function signIn() {

    provider = new GoogleAuthProvider();
    auth = getAuth();

    signInWithPopup(auth, provider)
        .then((result) => {
            // This gives you a Google Access Token. You can use it to access the Google API.
            credential = GoogleAuthProvider.credentialFromResult(result);
            token = credential.accessToken;
            // The signed-in user info.
            const user = result.user;
            email = user.email;
            document.getElementById('useremail').innerHTML = email;

            document.getElementById('loginbutton').style.display = "none";
            document.getElementById('shoppinglistpage').style.display = "block";

            displayShoppingList(email)

        });

}
Enter fullscreen mode Exit fullscreen mode

The code in index.html is somewhat "muddied" by the introduction of the "login" button, but I hope that you can work out what is going on. The display initially shows just a "login" button. Clicking this opens Google's login screen. Completing this successfully then reveals the ShoppingList page.

(d) Finally, to authorise Google login as a valid way of accessing the webapp, you need to go back to the Firebase console. Here you can use the "Authentication" entry under Project shortcuts in the console's left-hand panel for your project to access the "Signin-in-method" tab. Here you can then enable Google as a "permitted sign-in provider". I suggest you use your Google a/c email as the "Project support" email address at this stage.

If you now redeploy the webapp, you'll find that it displays a popup window that checks for the existence of a logged-in Google account on your device. If it finds one, the popup disappears and the application displays the shopping list for the logged in email. If it can't find one, the popup asks you to log in with a valid gmail account. Neat - this is seriously powerful IT and a great saver of development effort!

If the account used to access the webapp is new to the project (in which case, of course, the webapp will display an empty shopping list, ready for the user to create new purchase items), logging in also adds the account id to the Firebase console's list of application users for your project (thus allowing you to keep track of who's using it). You'll find this list under the Users tab of the Console's Authentication tool

Recognising that not everybody wants to use Google sign-in for authentication, Firebase offers numerous alternative sign-in providers such as Twitter and Facebook. But if you want to be a bit more conventional and customise your own arrangements for registering users, Firebase functions are available for this as well. You can see an example of this arrangement in the bablite.web.app pilot referenced earlier. Just start it up in the browser and "inspect" its index.js code to see how customised registration is achieved..

Google's documentation for the various signon methods can be found at

What else is there to say?

If you've been following this post just to try out the technology, you can give yourself a pat on the back and stop now - you've seen a seriously useful application, advertised on the web and secured from malicious activity.

But suppose you wanted to put this on a production basis with real users - perhaps users who are paying you for the privilege of using your app? In such a case you might want to look at the firebase emulator.

The firebase emulator: Want to make some changes to your code? How do you do this without upsetting your users while you test the changes? What you need is somewhere else to source the webapp and perhaps another database as well. The firebase emulator allow you to run your webapp from files on your own machine and, if you choose, run it against a local Firebase database. This sounds as if this might be rather difficult to arrange, but actually the firebase design makes it really straightforward by providing an "emulator" system. Once you've installed the emulator you'll find you have access to exactly the same facilities that you enjoy in the live Firebase console. It's easy to install and operate too.

If you've got a serious production webapp and want to keep ahead of the competition , you may also be concerned about efficiency. If you want your product to be "lean and mean" you need to look at the "tree-shaking" arrangements that Firebase 9 offers.

Webpack and "tree shaking": Google has really pulled out all the stops in version 9 to ensure that the code it produces meets the latest expectations for efficiency and resilience. Sadly, because the procedure I've described thus far uses "browser modules" the code as described above can't take advantage of the new arrangements. But once again, the procedure is easier to apply than you might imagine. Basically, it just boils down to reverting the code to reference "proper" modules and using a terminal session to run ẁebpack -a third-party piece of software - to produce a "compressed" version of the initial index.js file.This is then deployed in its place. It's really just a question of getting your "workflow" organised. You might also want to consider version control issues and bring Github into the picture as well.

A large webapp will need to cover a lot of ground - you'll need to work hard to keep the code tight and maintainable. Firebase "functions" let you both organise the code and spread the processing load.

Firebase Functions and Background tasks: It makes sense to configure certain elements of your application's operations as background events. An example might be the despatch of an email when a user signs up for a new account. Situations like this will arise in many different situations and, since these actions are generally "secondary" to the main purpose of their parent transaction, it makes sense to handle them as background tasks. Firebase "functions" enable us to code these background tasks in javascript and launch them in response to trigger events fired by their parent transactions.

There's a lot more to Cloud Services than Firestore databases. You may find you have a need for hosted "conventional" storage.

Cloud storage: How would you use your webapp to upload a conventional file into the Google cloud and read it back once it arrives there? Cloud Storage is available to provide an answer to this and any other storage requirements that don't conveniently fit into the database collection structures we've seen so far.

However, I think you've suffered enough for now. But once you've recovered, if you feel you'd like to take things to the next level, why not check out some of the more advanced posts in this series.

Top comments (2)

Collapse
 
invernomutogit profile image
Alessandro Piconi

Great post.
adding "&& request.auth.token.email == resource.data.userEmail;" to the database rules give me a console error like "Uncaught (in promise) FirebaseError: Missing or insufficient permissions".
deleting it all go fine.

Collapse
 
mjoycemilburn profile image
MartinJ • Edited

You're right - there 's a problem with my rules. I'm not sure how this slipped through but rules for "create" need to be a bit different to those for read, update and delete. I should have declared them as

service cloud.firestore {
  match /databases/{database}/documents {

    match /userShoppingLists/{document} {
        allow read, delete, update : if request.auth != null && request.auth.token.email == resource.data.userEmail;
        allow create : if request.auth != null && request.auth.token.email == request.resource.data.userEmail;
    }
}
Enter fullscreen mode Exit fullscreen mode

The "create" rule needs to recognise that this is a "pending" request as the data isn't actually in a document yet and can't be located at resource.data.userEmail

The "users should only see documents stamped by their user-id (userEmail)" stuff isn't really relevant to the main purpose of the post so I've edited this subtlety out