Automation Recipe: Memberstack x Airtable [Low-Code]

Nicolas Scott
Community Engineer
September 14, 2022
Try Memberstack for Free!

TABLE OF CONTENTS
GET STARTED
Try Memberstack

A step-by-step tutorial on using Memberstack Webhooks and Napkin.io cloud functions to automate adding new members to Airtable.

In my previous post, I introduced the concept of going serverless and its benefits. If you’ve outgrown tools like Zapier and Make or you’re simply interested in low-code scripting with 3rd party APIs, then going serverless is a simple and easy way to get started.

In this post, I’m going to teach you step by step how to harness the power of cloud functions to automate events in Memberstack and sync new members into Airtable.

#TL,SMTC - (Too Long, Show Me The Code)

👀 In a rush? Fork the cloud function & clone the Airtable base

Here’s what I’ll be covering:

  • Introduction to Napkin.io
  • Subscribing to Webhook Events in Memberstack 2.0
  • Best practices in setting up an Airtable database
  • Intro to Airtable’s Node.js SDK
  • Logging new Memberstack members in Airtable

By the end of this tutorial, you’ll have your own automation running in your own cloud function.The best part is, you don’t have to worry about servers, backends, or any complex tooling!

Now, before we get started, here’s a few things to keep in mind.This is a low-code tutorial, but you should still have basic familiarity with JavaScript and APIs.Also, if you end up using any of this code in production, make sure you’re available to keep an eye on the code from time to time. If not, have a developer handy.

Lastly, if you haven’t already, sign up for a Memberstack 2.0 account. We’re in open beta and its free to get started. I also recommend joining our Slack community. You can reach out to me (@nicolas) with any questions you encounter along the way.

Now that we got that out of the way, let’s start building.

Getting Started with Napkin

Our first step is creating an account on Napkin.io.

Napkin is by far my favorite “functions-as-a-service” (FAAS) provider. The annoying thing about AWS and Google Cloud is that their platforms come with a learning curve. Even Firebase Cloud Functions are annoying to setup. Napkin is practically zero-configuration.

No need to set up dev environments or install any tooling - with Napkin’s free plan, you get 25k API calls/month. You’ll get 500k API calls on their PRO plan, which is the only paid plan that they offer. They even have a visual editor so you can write your functions directly in the browser! This makes writing and deploying functions a breeze!

https://media.giphy.com/media/Du4mU3uDQHFsvw78PM/giphy.gif

When you’re done with the signup process, go ahead and create your first function.You’ll find that creating functions is pretty straightforward. That’s because it’s the only thing you can do on the platform. We love simplicity, right?

Next, you’ll be asked to name your function. Make it something related to the task. For example, I named my function “memberstack-new-member”.  Once you’ve finished naming your function, let’s add some code. Paste the following code in your function’s editor:


export default (req, res) => {
  res.json({"message": "Hello, world!"})
}

Now, click “run” to perform a test. Open the response tab in the bottom console and you should see the output:


{ ”message”: “hello, world!” }

If everything works, click the green “deploy” button in the top right corner. Now your function is live! That's all there is to it — you just wrote your first serverless function! 🎉

Now let’s use this function to catch a webhook from Memberstack.
Go ahead and copy the function URL at the top center of your screen. You’ll need this in the next step.

Subscribing to Webhooks In Memberstack

With Memberstack 2.0, you can subscribe to webhooks to listen for events that happen in your account. Currently, we offer webhooks for:

  • member events (created, updated, and deleted)
  • member plan events (plan added, plan updated, plan replaced and plan canceled)
  • member payment events (payment failed and payment succeeded)

To enable webhooks, navigate to the Dev Tools section in the dashboard and click “Add New Endpoint”. An important thing to note: you can toggle between live mode and test mode in Memberstack. I recommend using test mode for this tutorial.

Under the webhook configuration panel, select member.created in the Events column.Give your webhook a name (it’s helpful to include something related to the event you’re listening for and the app receiving the webhook). Next, paste the function URL you copied from Napkin into the URL section. Finally, Click Save & Publish.

Memberstack is now ready to send payloads of data to this destination URL every time a new member is created. Remember, this only applies in test mode. You’ll need to configure webhooks in live mode to listen for events in production apps.

Now, let’s head back to Napkin and update our function.

Preparing The Function to Receive Data

We’re going to inspect the payload of data that Memberstack sends when a new member is created. Replace all of your existing code with the following snippet:


export default async (req, res) => {
  try {
    // throw error if not a POST request
		if (req.method !== "POST") throw new Error(`Only POST methods allowed`)
		// extract payload and event from webhook
    let { payload, event } = req.body
    // return error if event type is not member.created
		if(event !== 'member.created') {
			let message = "Only member.created webhook payloads allowed"
			return res.status(400).json({ message })
		 }
     // log helpful data to inspect the payload
    console.log(`Incoming ${event} event for member: ${payload.id}`)
		console.log(`Email: ${payload.auth.email}`)
    console.log("Custom Fields: ", payload.customFields)
    console.log(`Plan connections: ${payload.planConnections.length}`)
    // return success message
    return res.status(200).json({ data: `Success. Processed ${event} event` })
  } catch (err) {
    console.log("error", err);
    return res.status(500).json({ "message": err })
  }
}

What’s happening here?

Basically we’re exporting a function handler that contains a request object (req) and a response object (res). Memberstack sends Webhooks as POST requests, so we want to make sure that our function  throws an error if the request method (req.method) is not equal to POST. If the method is a POST request, the code will continue running.


// throw error if not a POST request
if (req.method !== "POST") throw new Error(`Only POST methods allowed`)

Next, we want to make sure that we’re processing the correct event types.We can find this information the body of the request (req.body). The request body will include a payload of member data along with the event. Let’s return an error status code if the event is not equal to member.created.


...
// extract payload and event from webhook
let { payload, event } = req.body
// return error status if event type is not member.created
if(event !== 'member.created') {
	let message = "Only member.created webhook payloads allowed"
  return res.status(400).json({ message })
}

If the method is a POST request and the event type is member.created, the function will have passed the minimum requirements we need to process the code. We can now safely assume that member data will be attached the payload that we extracted from the request body.Let’s log some data to the console to inspect it:


// log helpful data to inspect the payload
console.log(`Incoming ${event} event for member: ${payload.id}`)
console.log(`Email: ${payload.auth.email}`)
console.log("Custom Fields: ", payload.customFields)
console.log(`Plan connections: ${payload.planConnections.length}`)
// return success message
return res.status(200).json({ data: `Success. Processed ${event} event` })

With this new code in place, we’re ready to catch a new member hook. Let’s go back to the Memberstack dashboard and create a new member for testing purposes.

Catching The Hook

Navigate to the Members tab on the sidebar (make sure you’re still in test mode) and click “Add Member” in the top right corner.

After creating a member, we should be able to see a new event in our Napkin function console. Let’s head back to Napkin and check it out. From the very top of the code editor view in your function, click on the Events tab.

You should see a recent event. The green POST symbol indicates that a POST request was recently sent to this function URL. Open it up and you’ll see some details on the request and the response, as well as any output from console logs.

In the request block, you’ll notice a bunch of headers along with the webhook payload that Memberstack sent. Below, in the response block, you should see the success message that our function returned. Finally, the console logs should print text with dynamically injected member data.

If you’re seeing all of this information, then congrats! You just caught your first Memberstack webhook in a cloud function. 🎉

We’re making progress. Now, let’s bring Airtable into the mix.

Setting Up Airtable

For this tutorial, I went ahead and created a base template with a Members table. You’ll find this resource already has all of the necessary fields that we’ll be using in our function.  You can clone that template here.  I strongly recommend using the template for the sake of keeping things separate and isolated while testing,

Open up the base and inspect the table. You’ll notice the following fields:

  • Id - this is the primary key column. It stores the member’s Memberstack ID.
  • Email
  • Created - a Unix timestamp
  • Friendly Time - a formula that converts the timestamp into a friendly date
  • Plans - comma separated values of member’s Plan IDs

Before we continue - a quick note on best practices with Airtable table schema.

I know some people who use “email” or “name” as the primary key (or identifier) in tools like Airtable and Google Sheets. I suggest not doing this if your primary goal is logging Memberstack members.

When it comes to CRMs or email marketing platforms, it makes perfect sense to treat email as the primary identifier. That’s because you’re dealing with a contact and not an app user. You likely don’t have another form of unique identification available in this context.

When it comes to users, we typically want to work with unique IDs whenever possible. This is standard practice and we do it because when a user signs up for an app, the database will always generate a unique ID them. This ID is persistent and never changes. Unlike emails, which users normally have the option of updating in the app.

The same applies to Memberstack; member IDs will persist until their accounts get deleted. This makes the Memberstack ID a perfect primary key (and universal identifier) for your Airtable Members table, as it will always be the same.

Once you successfully clone the base, its time to start adding members…But first, let’s setup our credentials to use the Airtable API in our function.

Adding The Airtable.js SDK

We’re going to use the official Airtable.js backend SDK to interact with our base. This SDK is a wrapper over their REST API and makes it much easier to perform table operations.

To connect to the Airtable.js SDK, you’ll need to provide the following information:

  1. Your Airtable API key
  2. Your Airtable Base ID
  3. Your Airtable table names

Generating an API key

To obtain your Airtable API key, go to Account settingsOverview and scroll to the API section. Grab this key and keep it somewhere safe. You won’t be able to view it again unless you generate a new key.With backend APIs, it’s absolutely critical that you don’t expose the values of your secret keys in any code. That’s because most backend APIs come with administrative ability and usually have the power to add, update and remove data on your account.

Finding Your Base ID

Your Airtable base ID can be found by opening your base and copying it from the URL, immediately after https://airtable.com/ . It starts with app.

Securing Credentials In Your Function

Before we update our function, we need to create something called environment variables.

Environment variables can be used for an number of different purposes. It’s common practice for developers to use environment variables for things like API endpoints, API keys, configuration settings and any other values that they want to set for that particular environment where the code is being executed.

In the context of this tutorial, we’ll use environment variables to prevent hardcoding and exposing our API credentials inside of our cloud function. Back in your Napkin function editor, open the tab labeled Other.  Scroll down until you see the section titled Environment Variables and click the Add button.

You’ll need to generate two variables for this cloud function to work; your Airtable Base ID and your Airtable API Key.  Copy the two variables names below (keeping the casing and underscore intact) along with their respective values.


AIRTABLE_BASE_ID="your base ID"
AIRTABLE_KEY="your API key"

Installing and Importing The Airtable SDK

Once you’ve set both variables, save and navigate to the Modules tab on the top navbar.Under the Add a Module section, search for “airtable”. Click the downwards pointing arrow to install it.

After installing the Airtable SDK, your function will be able to use all of the methods that are available in the API. Let’s go back to the code editor view and update our function now. At the very top of the editor, above our existing code, add the following snippet:


import Airtable from "airtable"

Airtable.configure({
    endpointUrl: 'https://api.airtable.com',
    apiKey: process.env.AIRTABLE_KEY
});

const base = Airtable.base(process.env.AIRTABLE_BASE_ID);

The top line imports our newly installed Airtable SDK and assigns it to the variable named Airtable.Now, we can use this namespace to access all of the API methods that come bundled in this SDK.

After the import statement, we need to configure the SDK to use our specific credentials. Notice instead of hardcoding the values, we are using process.env.AIRTABLE_KEY. This tells our function to reference the value stored in our AIRTABLE_KEY environment variable. Nice!

Lastly, we use Airtable.base(process.env.AIRTABLE_BASE_ID) to store a reference the base that we want to work with. In this case, its the base template that we cloned earlier. Now replace the rest of your code with the following snippet:


// airtable configuation is here

export default async (req, res) => {
  try {
    // throw error if not a POST request
		if (req.method !== "POST") throw new Error(`Only POST methods allowed`)
		// extract payload, timestamp and event from webhook
    let { payload, timestamp, event } = req.body
    // return error if event type is not member.created
		if(event !== 'member.created') {
			let message = "Only member.created webhook payloads allowed"
			return res.status(400).json({ message })
		 }
    let planIds = payload.planConnections.map(con => con.planId)
    let member = await base('All Members').create({
      "Id": payload.id,
      "Email":  payload.auth.email,
      "Created": timestamp,
      ...(planIds && {
        "Plans": planIds.join(', ')
      })
    })
    return res.status(200).json(member._rawJson)
  } catch (err) {
    console.log("error", err);
    return res.status(500).json({ "message": err })
  }
}

Let’s take a closer look at the first two new lines of code that we just added:


// we're now grabbing the timestamp from the webhook event too
let { payload, timestamp, event } = req.body
...
...

// return an array of planIDs associated with this member
let planIds = payload.planConnections.map(con => con.planId)
...

What’s this payload.planConnection.map stuff all about?  It’s pretty cool actually.

Mapping is a type of loop, like for or forEach, but you can only use it on arrays. What’s unique about the mapping method is that it returns a new array. That means you can use the map method to loop through an array of JSON objects, modify (or remove) the objects and return an entirely reformatted array.

In this case, I used map to loop through all of the member’s plan connections (found in the payload.planConnections array), stripping away all of the unnecessary properties from each plan connection object. Doing this allowed me to return a new array of Plan IDs only.

Here’s an example of my webhook’s original plan connection array:


planConnections = [
  {
    "planConnectionId": "con_sb_cl7ko27pg00oa0wi8e7w6h6b3",
    "planId": "pln_cku4gsei0000209l2hj3fdb4l",
    "status": "ACTIVE"
  },
  {
    "planConnectionId": "con_sb_cl7ko27pg00ob0wi8gxnsfv52",
    "planId": "pln_new-free-plan-1u100oco",
    "status": "ACTIVE"
  }
]

With map, I can simply do this:


// irerate through each item in plan connections and return a new array
// each iterable object will be called "con"
let myNewPlanArray = payload.planConnections.map(con => {
	// access each object being iterated on here
	// modify the object or return a new one
	return {
		planId: con.planId
	}	
})

The snippet above will return a new array of plan connection objects with only plan ID  as a value. planConnectionId and status will be excluded from each object because I didn’t return it.

Taking a step further, I can use an “arrow” function as shorthand to return my modified data.Notice that I don’t have to explicitly use the return statement anymore.


let newPlanIds = payload.planConnections.map(con => con.planId)
console.log(newPlanIds)
// returns
// ["pln_cku4gsei0000209l2hj3fdb4l", "pln_new-free-plan-1u100oco"] 

Also notice, I’m not returning an object anymore (because I removed the curly braces from my return statement), I’m now returning an individual plan ID on every iteration. This will give me an array of strings instead of an array of objects, which is exactly what I want. JavaScript is awesome.

Finally, this piece of code that we added is the most important:


// create new record in airtable
// map field names in Airtable to member values from the webook payload
let member = await base('All Members').create({
	"Id": payload.id,
	"Email":  payload.auth.email,
	"Created": timestamp,
	...(planIds && {
	  "Plans": planIds.join(', ')
	})
})

This block of code is responsible for creating the new member record in Airtable. Notice await base('All Members') - this is name of the Members table in our base.

Also look at the object that we are passing into the create method. This object is a set of key value pairs. Each key represents the name of a column in your Members table and each value is pulled from the webhook payload object.

We’re adding the webhook’s timestamp to mark the “Created” date. Technically, we could use a special field in Airtable that auto generates the date when the record was added. I think it makes more sense to use the webhook timestamp as the source of truth for creation date, since the timestamp represents the actual time the member was created in Memberstack.

The last field we’re populating is the Plans column. Remember that custom plan ID array we created in the previous step? Well because it’s an array of strings (and not objects), we can use another cool array method called join.

The join method can convert an array of strings to a series of comma separated values using [array].join(', '). The Plans column in Airtable wants to receive a friendly format of data (text), so this method is perfect for achieving that.

That’s basically it. The entirety of your function code should look like this:


import Airtable from "airtable";

Airtable.configure({
    endpointUrl: 'https://api.airtable.com',
    apiKey: process.env.AIRTABLE_KEY
});

const base = Airtable.base(process.env.AIRTABLE_BASE_ID);

export default async (req, res) => {
  try {
    // throw error if not a POST request
		if (req.method !== "POST") throw new Error(`Only POST methods allowed`)
		// extract payload, timestamp and event from webhook
    let { payload, timestamp, event } = req.body
    // return error if event type is not member.created
		if(event !== 'member.created') {
			let message = "Only member.created webhook payloads allowed"
			return res.status(400).json({ message })
		 }
    let planIds = payload.planConnections.map(con => con.planId)
    let member = await base('All Members').create({
      "Id": payload.id,
      "Email":  payload.auth.email,
      "Created": timestamp,
      ...(planIds && {
        "Plans": planIds.join(', ')
      })
    })
    return res.status(200).json(member._rawJson)
  } catch (err) {
    console.log("error", err);
    return res.status(500).json({ "message": err })
  }
}

Your function is ready to go! Let’s take it for a test drive.

Hit the green deploy button in your Napkin editor and then head back to your Memberstack dashboard. Create another test member.

You should see an almost instant entry logged in your Airtable. If so, then you just successfully created your own automation script! There wasn’t much code required either. If you’re not seeing any data, check the Events tab on Napkin and look for error messages.

In the final part of this series, we’ll integrate Webflow CMS. I’ll also release a separate tutorial exclusively for building this automation in Zapier and Make.

See you next time!