Implementing an Authenticated Backend API for your Firebase Hosting App with Cloud Functions.
TLDR; This post covers the basics of ..
- Deploying a static site using firebase hosting
- Creating a backend api using Firebase Cloud Functions (python sdk) (on the same domain using rewrite rules). This api can also be built with GCP Cloud Run
- Implementing basic authentication to restrict access to the api.
Firebase is an incredibly well-designed platform for building and deploying web apps. It is pretty versatile with products/tools for building your database, authentication, storage, hosting and even machine learning. It is also very easy to get started with and has a generous free tier
IMO, the core promise of Firebase is that it makes scaling very easy (while supporting production grade features), making it a good choice for a startup or side project that you see growing into something serious.
Firebase Hosting
Firebase is an app development platform backed by Google and trusted by millions of businesses worldwide. It helps you build and grow apps and games that users love.
Firebase provides a range of features to help developers build, release, monitor, and engage with their apps more efficiently. The Build feature simplifies backend setup and scaling, while the Release & Monitor feature streamlines testing, troubleshooting, and feature rollouts. The Engage feature offers tools to optimize user experience, understand user behavior, and tailor app customization.
In the past, I have written about how I have used Github pages to host static web pages with great success. Firebase Hosting offers similar capabilities - ability to host static content (CSS, HTML, JS, images, etc) and serve it over HTTPS with a custom domain. Firebase hosting enables fast content delivery - each file that is uploaded is cached on SSDs at CDN edges around the world and served as gzip or Brotli. Firebase hosting auto-selects the best compression method for your content - no matter where your users are, the content is delivered fast.
Firebase hosting also offers a generous free tier - 1GB of storage and 10GB of monthly bandwidth. This is more than enough for most personal projects and side projects. If you need more, you can upgrade to the Blaze plan which is pay-as-you-go.
Getting started is fairly easy. You can use the Firebase CLI to initialize your project and deploy your static content. You can also use the Firebase console to create a new project and deploy your static content.
# Install the Firebase CLInpm install -g firebase-tools# Login to Firebasefirebase login# Initialize your projectfirebase init # run this in your project directory# Deploy your static contentfirebase deploy #
The command above assumes you already have your static site setup e.g. using frameworks like Gatsby, NextJS, etc. Typically, once you are done creating your site, you should have a build
or public
directory with the compiled/optimized version of your site content. The firebase init
command above will create a firebase.json
file in your project directory. You can add a public
section to the firebase.json
file to specify the directory containing your static content. Running firebase deploy
will deploy your static content to firebase hosting.
{"hosting": {"public": "public"}}
Note that firebase provides a default url for your app which you can update to a custom domain. You can also use a custom domain with SSL/TLS certificates managed by firebase.
Firebase Functions
Cloud Functions for Firebase is a serverless framework that enables the automatic execution of backend code in response to Firebase events and HTTPS requests. With support for JavaScript, TypeScript and Python, this tool offers seamless integration with Firebase and Google Cloud features, such as Firebase Authentication and Cloud Storage Triggers. Developers can easily deploy their code to the managed environment, which automatically scales computing resources based on usage patterns. Additionally, Cloud Functions provide a secure and private platform to manage application logic, eliminating concerns about credentials, server configurations, and tampering from the client side.
As you build your applications, you may want functions that support capabilities like database CRUD operations, authenticated access to resources such as file download or other business/application logic. These can all be implemented as cloud functions. The first step is to initial firebase functions in your project
# firebase init functionsfirebase init functions # run this in your project directory, this creates a functions directory
Note that you will be walked through some questions e.g., on the deployment language (python, javascript, etc), - we will be using python here. Some boilerplate code will be created in the functions directory with a main.py file and a requirements.txt file.
Firebase functions support using a python wep api frame work like flask, which is convenient.
In the snippet below, I show an example firebase function written using flask.
from firebase_functions import https_fnfrom firebase_admin import initialize_app, authimport flaskfrom flask import jsonify, request, send_filefrom flask_cors import CORS, cross_originimport osinitialize_app()app = flask.Flask(__name__)# check if dev or prod, allow cors for local testingif os.environ.get("FUNCTIONS_EMULATOR", None) == "true":print("*********** dev mode")CORS(app)app.url_map.strict_slashes = False # allow trailing slash on routes@app.get("/api/stuff")def stuff():return flask.Response(status=201, response="Hello world")@https_fn.on_request()def flaskapp(req: https_fn.Request) -> https_fn.Response:with app.request_context(req.environ):return app.full_dispatch_request()
The snippet above makes the flaskapp
method the default handler for all requests to the cloud function. Note that we can now handle additional routes (get, post etc) on the flask app e.g., /api/stuff
which can be used to implement the backend api for your app.
Learn more here.
Note that the functions created/deployed, they can be triggered either by specific https requests on the cloud function url or by specific events in the firebase project (e.g., database events, authentication events, etc). In the next section, we will review how to add the /api/stuff
route on your firebase hosting domain.
Pairing Firebase Hosting and Functions to Create a Backend API
The final step here is to make the cloud functions available as routes on your firebase hosting app. This is done by adding a rewrites
section to your firebase.json
file.
{"hosting": {"public": "public","rewrites": [{"source": "/api/**","function": "flaskapp","region": "us-central1","pinTag": true}]}}
The rewrites
section above tells firebase hosting to route all requests to /api/**
to the flaskapp
cloud function. Remember to ensure all routes in your flask app above start with /api
e.g., /api/stuff
. Pretty neat!
Cloud Functions vs Cloud Run | When to Use What?
In the example above, we have chosen to use firebase functions to implement our backend api. However, if you are familiar with the Google Cloud Platform (GCP) ecosystem, you might be aware of Cloud Run which could also be used to implement the backend api.
And so an important question is - when to use Cloud Run vs Cloud Functions. Here are my findings so far:
Cloud Run is a managed compute platform that lets you run containers directly on top of Google's scalable infrastructure.
You can deploy code written in any programming language on Cloud Run if you can build a container image from it. In fact, building container images is optional. If you're using Go, Node.js, Python, Java, .NET Core, or Ruby, you can use the source-based deployment option that builds the container for you, using the best practices for the language you're using.
Cloud Run is serverless: it abstracts away all infrastructure management, so you can focus on what matters most — building great applications. Only pay when your code is running, billed to the nearest 100 milliseconds.
-
Cloud Run is a container based service that supports a variety of languages and frameworks (if your backend is some esoteric PROLOG language based component, as long as you can containerize it, you can deploy it). Cloud Functions is a serverless framework that supports a limited set of languages and frameworks (e.g., python, javascript, etc).
-
Cloud Run is billed based on the number of vCPU-seconds and GB-seconds used by the container. Cloud Functions is billed based on the number of invocations, compute time, and memory used. However, Cloud Run supports multiple concurrent connections which can be more cost effective for some use cases [^1].
-
Firebase also supports creation of backend apis using Cloud Run[^2] with support for rewrite rules (which we used above with cloud functions) that make the Cloud Run service available on your firebase hosting domain.
I am currently leaning on recommending Cloud Run mainly due to the flexibility of easily migrating your current workflow as a managed containerized service.
Securing Access to your Backend API (Firebase Auth Integration)
In many cases, some of the functionality on your api might be expensive to run (e.g., it involves calls to a machine learning server endpoint) or you might want to restrict access to only authenticated users. You can implement this by ensuring requests to your app have a valid firebase id token which you can validate on the backend. If your app uses firebase authentication (which is highly recommended), you can get the id token from the client and pass it in the request header.
In the snippet below, I show how you can modify main.py
to only respond to requests with a valid firebase id token.
from firebase_functions import https_fnfrom firebase_admin import initialize_app, authimport flaskfrom flask import jsonify, request, send_filefrom flask_cors import CORS, cross_originfrom functools import wrapsimport osinitialize_app()app = flask.Flask(__name__)# check if dev or prodif os.environ.get("FUNCTIONS_EMULATOR", None) == "true":print("*********** dev mode")CORS(app)app.url_map.strict_slashes = Falsedef verify_request(func):@wraps(func)def wrapper(*args, **kwargs):print('Check if request is authorized with Firebase ID token')# Check if token is present in headersauthorization = request.headers.get('Authorization')if authorization and authorization.startswith('Bearer '):print('Found "Authorization" header')token = authorization.split('Bearer ')[1]# Check if token is present in cookieselif request.cookies and request.cookies.get('__session'):print('Found "__session" cookie')token = request.cookies.get('__session')else:print('No Firebase ID token was passed as a Bearer token in the Authorization header, or no "__session" cookie')return jsonify({'data': {'status': 'Unauthorized'}, 'status':False, 'message': 'Unauthorized'}), 403try:# This will fail in every situation BUT successful authenticationdecode_token = auth.verify_id_token(id_token=token)print('ID Token correctly decoded', decode_token)request.user = decode_tokenexcept Exception as e:print('Error while verifying Firebase ID token:', e)return jsonify({'data': {'status': 'Unauthorized'}, 'status':False, 'message': 'Unauthorized'}), 403return func(*args, **kwargs)return wrapper@app.get("/api/stuff")@verify_requestdef stuff():return flask.Response(status=201, response="Hello world")@https_fn.on_request()def flaskapp(req: https_fn.Request) -> https_fn.Response:with app.request_context(req.environ):return app.full_dispatch_request()
The snippet above adds a verify_request
decorator to the stuff
method. This decorator checks if the request has a valid firebase id token and returns a 403 (not authenticated) error if not. Note that you may want to do more with the id token e.g., check if the user has the right permissions, etc. You can learn more about these sort of custom roles via firebase authentication custom claims.
And finally, you can deploy your entire app (static content and backend api) using the firebase cli.
# deploy static content and functionsfirebase deploy --only hosting,functions
Caveat - Firebase Can Be Expensive
Ofcourse, while Firebase is great, the one thing to ensure you are on top of is costs. Firebase is a pay-as-you-go service and it is easy to rack up a significant bill, so make sure you are monitoring your usage and costs. Some potential sources of growing costs include storage (e.g., if you are storing a lot of files in firebase storage), database reads/writes, function invocations, bandwidth (even cdn bandwidth is charged), etc.
Compared to tools like Netlify, Github Pages, etc, Firebase is more expensive. However, it also offers a lot more features and capabilities and an overall decent developer experience. Some things you can do:
- Set budget alerts to get notified if your costs exceed a certain threshold.
- Optimize your static content to reduce bandwidth usage (e.g., svg instead of png, tree shaking to reduce js bundle size, etc)
Conclusion
In conclusion, Firebase provides developers with a robust, scalable, and secure platform to build and grow applications. By integrating Firebase Hosting with Cloud Functions, it offers an efficient way to create a backend API for your app on the same domain. Restricting API access can also be implemented fairly easily using Firebase Auth. However, it's imperative to keep an eye on the Firebase costs, as it operates on a pay-as-you-go basis. For developers looking for a comprehensive development platform with strong support for static site hosting, back-end API construction, and authenticated access, Firebase emerges as a strong contender. However, it's crucial to plan, monitor, and manage the costs effectively to gain maximum benefits.
References
[^1]: Cloud Run VS Cloud Functions: What’s the lowest cost? [^2]: Firebase Hosting with Cloud Run
[^3]Cloud Run VS App Engine: What’s the lowest cost? https://medium.com/google-cloud/cloud-run-vs-app-engine-whats-the-lowest-cost-6c82b874ed61