Skip to content

Serverless App Using Firebase

Introduction

I have been writing about the application architecture without REST, which includes the underlying architecture using WebSockets and the database-driven architecture. In this post, I will continue the journey to make a serverless application architecture using Firebase and AppRun.

You will see how easy it is to use AppRun's event system with the Firebase stack to develop applications that have the full business logic process capabilities, such as authentication, authorization, request logging, and real-time database, and without REST layer.

Finally, we can make the application a serverless deployment to Firebase.

The Architecture

The example application uses the following technologies:

  • Firebase Cloud Firestore as the backend database
  • Firebase Cloud Functions for business logic process
  • Firebase Hosting to host the frontend
  • Firebase Authentication

Firebase is Google's mobile platform that helps you quickly develop high-quality apps and grow your business.

I will focus on the architecture instead of step-by-step instructions. If you are not familiar with the Firebase suite of products, please search google for the tutorials.

The architecture can be summarized in the diagram below.

Architecture

Figure 1. Architecture Diagram

Let's get into the details.

Event Pub-Sub Using FireStore

The center of the architecture is the Firebase Cloud Firestore. Firestore is a real-time database that keeps your data in-sync across client apps. When one client saves the data, FireStore pushes the data to all other clients.

In the AppRun applications, we use app.on to publish events. If we save the events to FireStore, the events can be handled by other applications. It is the step (1) shown in Figure 1 above.

Firestore also triggers Cloud Functions.

Business Logic Process Using Cloud Functions

Cloud Functions is Google Cloud's serverless compute platform. It runs on the server, not in the client apps. Therefore it is the best technology for business logic processing, authentication, and authorization. Functions are serverless. Functions run on Google's server, so we don't need to provision, manage, or upgrade the server.

The Functions are event-driven (the magic word, I love). Firestore can trigger Functions upon data updates. When we save the events into FireStore, FireStore triggers the Function to handle the events automatically. It is the step (2) in Figure 1.

Real-Time Data Sync Using FireStore.

During the Functions event handling, it writes the updated data back to FireStore (step (3) in Figure 1). FireStore pushes the update to the frontend applications (step (4) in Figure 1). The frontend application listens to FireStore changes and publishes AppRun events for the frontend logic process to run.

Now, the event handling cycle is completed. Let's see it in action with an example.

Example

The example is a ToDo application.

Todo App

Figure 2. ToDo Application

Save Events to FireStore

As usual, in the AppRun applications, we convert the DOM events into AppRun events. E.g., When users click the add button, we publish the //: event.

// in JSX
<button $onclick={[add]}>Add</button>

const add = () => {
  app.run('//:', '@create-todo', {
    title: (document.getElementById('new_todo').value,
    done: 0
  })
}

The //: event handler saves the event into FireStore.

const db = firebase.firestore();
app.on('//:', (event, data = {}) => {
  db.collection(`events`).add({ uid, event, data })
});

There is a top-level collection, called events in FireStore. We save the user id (obtained using Firebase anonymous authentication), event name (@create-todo), and event parameters (the new to-do item).

FireStore triggers our Function, which is monitoring the events collection.

Handle Events in Functions

exports.updateTodo = functions.firestore.document('events/{Id}')
  .onWrite((change, context) => {
    const dat = change.after.data() as any;
    const { uid, event, data } = dat;
    const db = admin.firestore();
    const todos = db.collection('/users/' + uid + '/todos');
    switch (event) {
      case '@create-todo': return todos.add(data);
      case '@update-todo': ...
      case '@delete-todo': ...
      case '@delete-all-todo': ...
      default: return;
    }
});

The Function destructs the user id, event name, and event parameters and handles it accordingly, e.g., it adds a new Todo item data into FireStore upon the '@create-todo' event. And so on so forth.

FireStore then pushes the data change to the frontend.

Real-Time Data in Frontend

In the frontend, we subscribe to the onSnapshot of FireStore and publish the AppRun event, '@show-all'.

const db = firebase.firestore();
db.collection(`users/${uid}/todos`).onSnapshot(snapshot => {
  app.run('@show-all',
    snapshot.docs.map(d => ({ id: d.id, ...d.data() })))
});

Now, we are back to our AppRun application world, in which you can see the three familiar parts: state, view, and update.

import app, { Component } from 'apprun';

const state = {
  filter: 0,
  todos: []
}

const add = () => {
  app.run('//:', '@create-todo', {
    title: (document.getElementById('new_todo').value,
    done: 0
  })
};
const toggle = (_, todo) => { app.run('//:', '@update-todo', { ...todo, done: !todo.done }) };
const remove = (_, todo) => { app.run('//:', '@delete-todo', todo) };
const clear = () => { app.run('//:', '@delete-all-todo') };

const view = ({todos}) => {...}

const update = {
  '@show-all': (state, todos) => ({ ...state, todos })
}

The Firebase ToDo application shares the same architecture as in the Database-Driven Application Post. They are only different in events. The Firebase ToDo application saves the events to FireStore. The Database-Driven Application sends and receives the events through the WebSockets.

If you are new to AppRun, read the AppRun Book or visit AppRun Docs.

Live Demo and Source Code

You can play with the live demo at https://apprun-demo.firebaseapp.com.

Source Code: https://github.com/yysun/apprun-firebase

Conclusion

The AppRun event pub-sub pattern looks so simple (just app.run and app.on), yet so powerful. It is not only useful inside the frontend app. It shines more in crossing process boundaries, such as in the cases of WebSockets, Web Workers, Electron Apps, Firebase of course, and more ...