Sending transactional emails via Firebase and Cloud Functions
In this tutorial, we’ll walk through how you can start sending transactional emails through Firebase utilizing Google Cloud Functions.
Quick intro to Firebase and Cloud Functions #
Firebase #
Google Firebase offers a number of products that help speed up the development of both mobile and web applications. The cornerstone product is their real-time database which offers a No-SQL style JSON store. It’s pretty magical to work with. Combine it with their free authentication service, and you can build entire applications with just front-end code (HTML, CSS, JS).
Cloud Functions #
Google Cloud Functions takes all of this a step further by providing a way to queue up code to run behind the scenes, completely removing the need for a backend server. Functions are written in Node so that you can import any existing node modules out of the box.
Code inside a Cloud Function just sits there until it’s triggered. You can trigger Cloud Functions a few different ways:
- Realtime Database Triggers - by adding/editing/removing items from the DB
- Firebase Authentication Triggers - by adding/removing users
- Cloud Storage Triggers - by uploading/updating/deleting files & folders
- HTTP Triggers - on
GET
,POST
,PUT
,DELETE
, andOPTIONS
HTTP calls
The code you write inside a Cloud Function has admin access to everything within your Firebase account. Based on a trigger you can do lots of things, like get/set data from the database, or update a file in cloud storage. With HTTP triggers, once the function runs, you can even return data to the browser client.
For this tutorial, we’ll set up a simple Firebase database via JavaScript and use the real-time database trigger to fire off a transactional email via Postmark.
File structure #
Here’s a structural overview of how we’ll be organizing the code for this project.
- functions
- db
- users
- onUpdate.f.js
- public
- admin
- index.html
- google-signin.png
- index.html
Setting up authentication #
First, If you’ve not done so already, you’ll need to head into the authentication section within your Firebase project and activate Google as a sign-in provider:
Next, let’s write some code to log in via Google OAuth. We’ll place this file at the root of the public folder.
/index.html
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8">
<title>Postmark + Firebase Sample App</title>
<style>
body {
font-family: sans-serif;
text-align: center;
}
.conatiner {
margin: 80px auto;
width: 300px;
}
</style>
</head>
<body>
<div class="container">
<h2>Sign Up/In with Google</h2>
<a href="#" class="form--google-signin"><img src="/img/google-signin.png" /></a>
</div>
<!-- 1. Include jQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
<!-- 2. Initialize Firebase -->
<script src="https://www.gstatic.com/firebasejs/4.12.0/firebase.js"></script>
<script>
// TODO: Replace with your project's customized code snippet
var config = {
apiKey: "<API_KEY>",
authDomain: "<PROJECT_ID>.firebaseapp.com",
databaseURL: "https://<DATABASE_NAME>.firebaseio.com"
};
firebase.initializeApp(config);
</script>
<!-- 3. Sample authentication code -->
<script type="text/javascript">
(function($){
'use strict';
// ----------------------------------------------------------
// VARIABLES
// ----------------------------------------------------------
var auth = firebase.auth();
// ----------------------------------------------------------
// READY
// ----------------------------------------------------------
$(window).load(function(){
// Kick off events
initEvents();
});
// ----------------------------------------------------------
// EVENTS
// ----------------------------------------------------------
function loginEvents() {
// Log in or Sign up
$('.form--google-signin').on('click', function() {
authFirebaseInit();
return false;
});
}
// ----------------------------------------------------------
// AUTHENTICATION
// ----------------------------------------------------------
function authFirebaseInit() {
// Attempt to authenticate via Google
var provider = new firebase.auth.GoogleAuthProvider();
// We'll use a popup, but you can also auth in a new tab
auth.signInWithPopup(provider).then(function(result) {
// Redirect to admin
window.location = '/admin';
}).catch(function(error) {
// TODO: Error handling code here
console.log(error);
});
}
})(jQuery);
</script>
</body>
</html>
A few notes about this code:
- I’ve included jQuery here to simplify things, but it’s by no means mandatory.
- You’ll want to update the code in this section with the code that you received after setting up your Firebase project.
- The
authFirebaseInit()
function does the heavy lifting here. Upon success, it redirects the user to/admin
. Failures are logged in the console. If this is for a real-world project, you could add some error handling within the error catch section.
In a browser, you should see:
Once you click the blue sign in button, you’ll be prompted with the Google OAuth login popup.
Admin section #
Once you’ve logged in, you should be redirected to /admin
:
/admin/index.html
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8">
<title>Postmark + Firebase Sample App</title>
</head>
<body>
<!-- Include jQuery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js"></script>
<!-- Initialize Firebase -->
<script src="https://www.gstatic.com/firebasejs/4.12.0/firebase.js"></script>
<script>
// TODO: Replace with your project's customized code snippet
var config = {
apiKey: "<API_KEY>",
authDomain: "<PROJECT_ID>.firebaseapp.com",
databaseURL: "https://<DATABASE_NAME>.firebaseio.com"
};
firebase.initializeApp(config);
</script>
<!-- Quick example of real-time database code -->
<script type="text/javascript">
(function($){
'use strict';
// ----------------------------------------------------------
// VARS
// ----------------------------------------------------------
var auth = firebase.auth(),
fbdb = firebase.database();
// ----------------------------------------------------------
// READY
// ----------------------------------------------------------
$(window).load(function(){
// Check for user, else redirect to marketing site
init();
});
// ----------------------------------------------------------
// INIT
// ----------------------------------------------------------
function init() {
auth.onAuthStateChanged(function(user) {
// 1. Check to make sure the user is logged in
// If they're not, redirect them to the homepage
if (user) {
userInit(user);
} else {
// User is signed out.
// Redirect to homepage
window.location = '../';
}
}, function(error) {
console.log(error);
});
}
// ----------------------------------------------------------
// User
// ----------------------------------------------------------
function userInit(user) {
var userData;
// Check for user
fbdb.ref('/users/' + user.uid).once('value').then(function(snapshot) {
// 2. Add new user to DB if they don't already exist
if (snapshot.val() === null) {
// This data is automatically pulled from Google when you auth
userData = {
displayName: user.displayName,
email: user.email,
photoURL: user.photoURL
};
// 3. Save user data
fbdb.ref('users/' + user.uid).set(userData).then(function() {
console.log('User data saved!');
}).catch(function(error) {
console.log(error);
});
} else {
console.log('User details already added!');
}
}).catch(function(error) {
console.log(error);
});
}
})(jQuery);
</script>
</body>
</html>
Organizing Cloud Functions #
Cloud functions are developed locally and deployed via the command line. There are a number of different ways that you can organize them. Here’s the approach I take—which I picked up from Tarik Huber:
/functions/index.js
'use strict';
/** EXPORT ALL FUNCTIONS
*
* Loads all `.f.js` files
* Exports a cloud function matching the file name
* Author: David King
* Edited: Tarik Huber
* Based on this thread:
* https://github.com/firebase/functions-samples/issues/170
*/
const glob = require("glob");
const camelCase = require("camelcase");
const files = glob.sync('./**/*.f.js', { cwd: __dirname, ignore: './node_modules/**'});
for(let f=0,fl=files.length; f<fl; f++){
const file = files[f];
const functionName = camelCase(file.slice(0, -5).split('/').join('_')); // Strip off '.f.js'
if (!process.env.FUNCTION_NAME || process.env.FUNCTION_NAME === functionName) {
exports[functionName] = require(file);
}
}
Nothing needs to change in this file. Props to David King for coming up with this approach. It uses globbing to search the functions folder for files ending with .f.js
and auto-includes them keeping everything nice and tidy.
Watching for database changes & sending a transactional email via Postmark #
The last step is to write the code for the Cloud Function itself:
/functions/db/users/onUpdate.f.js
// 1. Deploys as dbUsersOnUpdate
const functions = require('firebase-functions')
const nodemailer = require('nodemailer')
const postmarkTransport = require('nodemailer-postmark-transport')
const admin = require('firebase-admin')
// 2. Admin SDK can only be initialized once
try {admin.initializeApp(functions.config().firebase)} catch(e) {
console.log('dbCompaniesOnUpdate initializeApp failure')
}
// 3. Google Cloud environment variable used:
// firebase functions:config:set postmark.key="API-KEY-HERE"
const postmarkKey = functions.config().postmark.key
const mailTransport = nodemailer.createTransport(postmarkTransport({
auth: {
apiKey: postmarkKey
}
}))
// 4. Watch for new users
exports = module.exports = functions.database.ref('/users/{uid}').onCreate((event) => {
const snapshot = event.data
const user = snapshot.val()
// Use nodemailer to send email
return sendEmail(user);
})
function sendEmail(user) {
// 5. Send welcome email to new users
const mailOptions = {
from: '"Dave" <dave@example.net>',
to: '${user.email}',
subject: 'Welcome!',
html: `<YOUR-WELCOME-MESSAGE-HERE>`
}
// 6. Process the sending of this email via nodemailer
return mailTransport.sendMail(mailOptions)
.then(() => console.log('dbCompaniesOnUpdate:Welcome confirmation email'))
.catch((error) => console.error('There was an error while sending the email:', error))
}
A few notes about this code:
- This is the code that actually sends the email. We use the nodemailer node module in addition to the nodemailer-postmark-transport node module which extends nodemailer with a custom transport.
- I’ve wrapped the
admin.initializeApp()
function in a try statement so that it only get’s instantiated once (else you might see errors in the cloud functions log). - Always use environmental variables for sensitive data that is needed in Cloud Functions.
- We’re watching for any new users that are added to
/users/{uid}
. - Whenever a new user is added, we send off an email.
- The
nodemailer
module does its thing. Theconsole.log()
function can be used anywhere within Cloud Functions to output data to the Firebase Cloud Functions event log.
Download the source code #
You can download/fork the source from this quick tutorial from this GitHub repo.
Enjoy! 🤟