Serverless Subscribe

Posted by Nathan Cahill on

Firebase’s new Cloud Functions make it really easy to implement a free mailing list subscription backend without a server. The frontend code is incredibly light, just 4 lines of JS:

var x = new XMLHttpRequest();
x.open('POST', 'https://nathan-cahill.firebaseio.com/mailing.json');
x.setRequestHeader('Content-Type', 'application/json');
x.send(JSON.stringify({email: f.email.value}));

A single API call is fired when the Subscribe form is submitted, which will POST the email address to a Firebase Realtime Database called mailing. Two rules are set on the Firebase database. One .write rule for “append writes only” permissions and one .validate rule for light email validation (email address must have @ in it):

{
"rules": {
"mailing": {
"$email": {
".write": "!data.exists()",
".validate": "newData.hasChild('email') && newData.child('email').isString() && newData.child('email').val().contains('@')"
}
}
}
}

Once the email has been written to the database, it triggers a simple Cloud Function. This function is deployed to Firebase by running firebase deploy --only functions:

var functions = require('firebase-functions');
exports.subscribe = functions.database.ref('/mailing/{pushId}/email')
.onWrite(event => {})

The new email address will be available in event.data.val(), although it requires checking for null first, since DELETE will trigger the write hook too.

The Mailing List

My mailing list is stored in Mailgun, so when a new subscriber is added, they must be added to the Mailgun list. It’s quite simple to call the Mailgun API from the Firebase Cloud Function:

var request = require('request');
var API_KEY = '';
var BASE_URL = 'https://api.mailgun.net/v3';
request.post({
url: BASE_URL + '/lists/[email protected]/members',
auth: { user: 'api', pass: API_KEY },
formData: { 'address': event.data.val() },
})

Note: Outgoing API calls require that your billing information be added to Firebase for verification. A low-traffic service like this can be run well within the free tier of the Blaze plan (2,000,000 invocations/per month limit).

Firebase Cloud Functions wants a promise to be returned for async functions, so we can wrap it all up like this:

var functions = require('firebase-functions');
var request = require('request');
var API_KEY = '';
var BASE_URL = 'https://api.mailgun.net/v3';
exports.notify = functions.database.ref('/mailing/{pushId}/email')
.onWrite(event => {
if (!event.data.val()) {
return
}
return new Promise((resolve, reject) => {
request.post({
url: BASE_URL + '/lists/[email protected]/members',
auth: { user: 'api', pass: API_KEY },
formData: { 'address': event.data.val() },
}, (err, response, body) => {
if (err) {
console.error(err)
reject(err)
}
resolve(body)
});
});
});

It’s easy to add more API calls in the same function, just return a Promise.all() wrapper around an array of promises. For example, you might want to be notified when someone signs up for your mailing list. Or send them a welcome email.

Future Proofness

If/when Firebase is shut down, this code is very easy to port to a simple server with a Redis List as a data store. Firebase allows dumping the database as JSON, which would be easily imported. The single API call to Firebase would be replaced by a service that takes the POST email address, inserts it into Redis and then fires off the appropriate Mailgun API calls.

The Mailgun functionality should be equally painless (who am I kidding, email is not painless, please don’t shut down Mailgun) to transition to another SMTP server. Meanwhile, I don’t have to worry about maintaining a server for something this simple.

Other Options

  • Mailchimp/Et al.: I’ve tried almost all of the mailing list SaaS apps. None of them are both highly customizable and free, and the JS they want to insert in a page to collect email addresses is huge (compared to my 4 lines). Especially Mailchimp, which I’ve felt like I’m fighting against every step of the way.
  • Mailgun only: Mailgun’s API authorization isn’t fine-grained enough to allow “append-writes only” to the mailing list.
  • $5 Digital Ocean droplet: Until Firebase or Mailgun shut down, I’d rather not admin a server. Even one as easy as a DO droplet.
  • AWS Lambda or Google Cloud Functions: Firebase’s solution is incredibly simple compared to the more involved (but also more powerful) offerings from AWS and Google Cloud. If this was for a client, I would probably opt for one of those options that will probably be around more permanently.

Conclusion

This highly-flexible, free and hackable setup is pretty much perfect for me. On the next installment, I’ll cover how I use React to statically generate this Hugo theme, how the mailing list is posted to when a new blog post is written, and how comments are integrated with Github.


Join the mailing list

I send infrequent updates about upcoming projects, and links to interesting mapping and data visualization tools.

Thanks for subscribing.
Something went wrong, double-check that your email address is corret.