How to build a Firebase Extension

Disclaimer: As of today, Firebase Extensions are an experimental feature in the platform and the way they work might change or even get removed from Firebase.

What are Firebase Extensions?

Firebase Extensions are the easiest way of pack and reuse Firebase Functions. That’s it. An extension is a packaged function that get installed in your Firebase project (one or multiple times) and that can be configured from the command line or the admin interface.

Firebase offers a set of extensions that have been growing on time, from sending emails, to process payments or translate text. Someone already created those functions for you and you just need to install and configure them in your projects.

You can request to become a publisher of extensions, this post is not about that, especially since it’s not clear to me how you apply to become a publisher. This post is about how to build one and test it.

Let’s build a Firebase Extension: Reverse geocoding

Let’s build a extension that given a document from your Firestore database with latitude and longitude, will reverse geocode those coordinates into a human readable address.

Firstly, mention that I wasn’t able to find much documentation about how to build them. So what I did is simple, use the power of Open Source and read the code:

You can find the codebase for this extension in this github repo.

First thing if you are going to work with extensions, tell firebase you want to play around with them, remember, is still a feature in preview mode:

$ firebase --open-sesame extdev

Let’s use the extension initializer that is, so far, hidden in the command line:

$ firebase ext:dev:init

After running that command and following some questions, you have an extension that is an example ready to modify:

Extension template created with firebase ext:dev:init

Your project structure will be:

  • CHANGELOG.md: Record your changes and different versions. Add release notes and those will appear in the Firebase Admin panel.
  • PREINSTALL.md: Documentation in markdown format that will be presented to the customer before installing the extension. The content of this file will be displayed by the command firebase ext:info. Useful if you plan to publish your extension.
  • POSTINSTALL.md: This file provides your users an overview of how to use your extension after they’ve installed it. All content is optional, but this is the recommended format. Your users will see the contents of this file in the Firebase console after they install the extension.
  • extension.yaml: This is the most important file. It describes the resources that your extension is exposing, like function or multiple functions. Also, declares the most important part of an extension, the parameters that you can use to configure the extension and adapt it to your needs. Unfortunately, all links related to documentation in the template are broken, so learning more about this file, and the correct format was a reading exercise from the extensions already created.
  • functions: This directory will contain your Firebase functions.

Our extension.yaml

Our extension transform latitude and longitude into a human readable address using Google Maps SDK. If we want to make it reusable we need to make it flexible.

To achieve that we will do the following:

  • We will define a function that will be triggered every time an document is created or updated in an specific Firebase path. This path must be configurable so users of our extension can adapt it to their needs. We will define a variable, COLLECTION_PATH, to configure this.
  • We should read latitude and longitude from fields in that document. Those fields should be configurable too (for example, they could be plain fields like ‘latitude’ or could be part of a Map ‘geo.latitude’). We will define two variables, LATITUDE_FIELD_NAME and LONGITUDE_FIELD_NAME, to let our extension know where to get latitude and longitude values.
  • The result should be stored in an configurable field of the original document. We will define the variable OUTPUT_FIELD_NAME to define where to store the address information.
  • Finally we will be using the Google Maps SDK to perform the reverse geocoding operation. An extra configurable parameter will be used here to each developer using your extension will need to provide their own. For this purpose we will define the variable GOOGLE_MAPS_API_KEY.

Here is our extension.yaml file:

name: firestore-extension-reverse-geocoding  # Identifier for your extension
version: 0.0.1  # Follow semver versioning
specVersion: v1beta  # Version of the Firebase Extensions specification

# Friendly display name for your extension (~3-5 words)
displayName: Reverse Geocoding

# Brief description of the task your extension performs (~1 sentence)
description: >-
  Given a document with a latitude and longitude, add an attribute to the document with the address for that point.

license: Apache-2.0  # https://spdx.org/licenses/

# Public URL for the source code of your extension
sourceUrl: https://github.com/firebase/firebase-tools/tree/master/templates/extensions

# Specify whether a paid-tier billing plan is required to use your extension.
# As we will performing calls to an external service the paid-tier will be necessary for this extension.
billingRequired: true

# In a `roles` field, list any IAM access roles required for your extension to operate.
roles:
  - role: datastore.user
    reason: Allows the extension to write to your Firestore Database instance.

# In the `resources` field, list each of your extension's functions, including the trigger for each function.
resources:
  - name: fsreversegeocoding
    type: firebaseextensions.v1beta.function
    description:
      Listens for new documents on the path specified via configuration, will look for latitude and longitude
      parameters and resolve them to a concrete address.
    properties:
      location: ${LOCATION}
      runtime: nodejs12
      eventTrigger:
        eventType: providers/cloud.firestore/eventTypes/document.write
        resource: projects/${PROJECT_ID}/databases/(default)/documents/${COLLECTION_PATH}/{messageId}

# In the `params` field, set up your extension's user-configured parameters.
params:
  - param: LOCATION
    label: Cloud Functions location
    description: >-
      Where do you want to deploy the functions created for this extension?
      You usually want a location close to your database. For help selecting a
      location, refer to the [location selection
      guide](https://firebase.google.com/docs/functions/locations).
    type: select
    options:
      - label: Iowa (us-central1)
        value: us-central1
      - label: South Carolina (us-east1)
        value: us-east1
      - label: Northern Virginia (us-east4)
        value: us-east4
      - label: Belgium (europe-west1)
        value: europe-west1
      - label: London (europe-west2)
        value: europe-west2
      - label: Frankfurt (europe-west3)
        value: europe-west3
      - label: Hong Kong (asia-east2)
        value: asia-east2
      - label: Tokyo (asia-northeast1)
        value: asia-northeast1
    default: us-central1
    required: true
    immutable: true

  - param: COLLECTION_PATH
    label: Collection path
    description: >
      What is the path to the collection that contains the latitude and longitued that you want to analyze?
    example: items
    validationRegex: "^[^/]+(/[^/]+/[^/]+)*$"
    validationErrorMessage: Must be a valid Cloud Firestore Collection
    default: items
    required: true

  - param: LATITUDE_FIELD_NAME
    label: Latitude field name
    description: >
      What is the name of the field that contains latitude value. Accepts dot notation.
    example: lat
    default: lat
    required: true

  - param: LONGITUDE_FIELD_NAME
    label: Longitude field name
    description: >
      What is the name of the field that contains the longitude value. Accepts dot notation.
    example: lon
    default: lon
    required: true
  
  - param: OUTPUT_FIELD_NAME
    label: Output field
    description: >
      What is the name of the field where you want to store your reverse geocoding components.
    example: reverse_geo
    default: reverse_geo
    required: true

  - param: GOOGLE_MAPS_API_KEY
    label: Google Maps Key
    description: >
      Google Maps Api Key used to request the reverse geocoding.
    example: XXX
    default: null
    required: true

Fetching our configuration

Once we install the extension we will need to configure and give value to those parameters. How do we fetch that data? Easy, through environment variables: process.env.

You can request this values at will, we will store them in a simple object in a file called config.js:

"use strict";

module.exports.default = {
  location: process.env.LOCATION,
  latitudeFieldName: process.env.LATITUDE_FIELD_NAME,
  longitudeFieldName: process.env.LONGITUDE_FIELD_NAME,
  outputFieldName: process.env.OUTPUT_FIELD_NAME,
  apiKey: process.env.GOOGLE_MAPS_API_KEY,
};

Building our main function

In our extension.yaml file we defined which resources our extension will be providing, and we name them. In our case we named a function called fsreversegeocoding, so that will be the function exported in our index.js file.

As well, for those familiar with Firestore, there are specific handlers for dealing with changes in documents, so our function will look like:

module.exports.fsreversegeocoding =
    functions.handler.firestore.document.onWrite(async (change) => {
        // do your stuff here
    }
);

We will use node-geocoder for calling the Google Maps API and Firebase SDK to store the results.

Take a look to the final result here.

Installing your extension

Let’s try our extension in one of our projects. We will do that from command line, from the directory containing the extension. Remember we are not distributing this extension, so if you want to install it download the source code and:

  • Remember to enable the firebase beta features if you didn’t do it before:
$ firebase --open-sesame extdev

Install the extension and provide the values for the configurable variables:

$ firebase ext:install . --project <your_project_name>

When you execute the install command you will get prompt for all the configurable attributes that you defined in the extension.

And from here, you’ll be able to interact and configure your extension from the Firebase Console.

Leave a Reply

Your email address will not be published. Required fields are marked *