The purpose of this article is to describe the architecture and implementation of a cost-effective and basic email marketing platform, It’s easy to manage, relatively easy to build, and scalable. It’s a great solution for startups or a business unit to keep in touch with their clients.
I won’t be talking about acquiring emails, opt-in approaches, or the regulatory aspects of mass emailing. My opinion is that email acquisition should be approached with integrity. The popular email hosts have effectively implemented spam blocking and a reputation measurement, so to acquire emails otherwise, seems like a waste of time. But we’ll leave that subject for another post!
The primary components of the solution we’ve built include:
We were already familiar with the SendGrid Email API for individual sends that we integrate commonly with Stripe. Therefore, when we decided to add an email marketing platform to our process, SendGrid Marketing was an obvious choice. We were specifically interested in SendGrid’s dynamic templates and SendGrid’s automation functionality. The dynamic templates are available across plans, allowing you to pass (auth codes, names, products, quantities, etc.) to the template and trigger an individual send.
They have plans that run from free to over $900.00/month with a ‘Basic’ and ‘Advanced’ delineation. We were interested in automating a series of emails to a specific list of contacts and this put us into the ‘Advanced’ category where for 10k contacts and 50k emails you start at $60/month.
The automations are interesting because they can be triggered by adding an email to a predefined list, which is a subset of the complete contact list that you’re being charged for.
You simply create a list, add an email address to that list and it triggers an automated send of 1 to n predefined emails with a preset time period between each send.
That looks great but would the API allow us to manage this directly from our front end? The short answer is yes. But they’ve designed them to make a few tasks overly complicated. I’ll get to that later, but for the moment the SendGrid Event Webhook worked well allowing us to capture the standard events (delivered, blocked, bounced, opened, clicked, etc.), and the SendGrid API allowed us to add/update/delete from our contact lists which could in turn drive the automations.
This was a difficult choice, you have plenty of other options. We’ve used Firebase, our own servers, and a bunch of other public options, but I wanted to experiment with Supabase. Their reputation seems to be positive and improving. The free plan was feature rich and accessible. It was all I needed to get the architecture in place and I’d set up a basic test in no time. But… Setting up RLS (Row Level Security) with confidence is difficult, and borders on incomprehensible. And the Supabase AI RLS-wizard-help-assistant-tool, must be alpha-alpha, because it only added to my confusion. Poke around enough and you’ll get it working but you won’t have much confidence that it’s policies correspond to your needs. If you’re working on anything sensitive, make sure you dig into this with patience.
Supabase is storing our parsed events from the Sendgrid Webhook. The SendGrid Webhook documentation is here. The idea was to store individual mail events into the Supabase tables, extract and prioritise the contacts associated to a specific mailing. For example a ‘unique click’ on one of my email links, makes THIS event, just a bit more valuable than an unopened delivery event. Maybe I’ll add a specific ‘weight’ to this contact. Keep in mind the event we’re writing into a Supabase events table is for a specific contact, and a specific email automation. The next event might be for the same contact yet from a different automation. I can gather and infer a lot of data from this basic event tracking.
I’ll go into more detail later, but writing and exposing the API which handled the SendGrid Webhook was the key challenge. If you’re familiar with Nuxt 3, you drop an EventHandler into the server/api directory which parses the Webhook, checks SendGrid for the contact id and writes the email, timestamp, and Automation name to Supabase. The code would look something like this:
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.SUPABASE_URL!;
const supabaseKey = process.env.SUPABASE_KEY!;
const supabase = createClient(supabaseUrl, supabaseKey);
interface EventData {
email: string;
timestamp: number;
event: string;
category: string[];
mc_auto_name: string;
}
export default defineEventHandler(async (event) => {
const body = await readBody(event);
if (Array.isArray(body)) {
for (const item of body) {
if (item.event) {
const listIds = await findContactId(item);
await insertEventToSupabase(item, listIds);
}
}
}
return { success: true };
});
async function insertEventToSupabase(eventData: EventData, listIds: string[]){ ... }
async function findContactId(eventData: EventData) { ... }
You’re familiar with this fully managed hosting solution. I’m a fan. I’d highly recommend configuring your Repo to work with DO’s App Platform. It’s a major time-saver.
Say it again…
Now that you see the different elements, lets do this again.
Start with the Supabase db, add an events table, and set up RLS. Prepare your SendGrid Webhook, capture it with a Nuxt API, parse and write the event to Supabase, build out a front-end in Nuxt 3. That’s it!
Starting with just individual events from SendGrid you can build out contact lists, statistics, automation triggers, list builders, etc. There is a huge potential for interacting directly with SendGrid based on metrics or unique filters which you might define and discover on the front end. For example, I’ve set up a bunch of filters and one of them shows me users who have opened the email multiple times but never clicked on a link. I want those users to cycle into another list – an automation – which is maybe more ‘click motivating’! You get the idea.