You can now subscribe to my email list on victoria.dev! Here’s how I lovingly built a subscription sign up flow with email confirmation that doesn’t suck. You can too.
If you’re interested in managing your own mailing list or newsletter, you can use Simple Subscribe to collect email addresses and create a list. This list is stored in a DynamoDB table, ready to be queried or exported at your leisure.
When someone signs up, they’ll receive an email asking them to confirm their subscription. This is sometimes called “double opt-in,” although I prefer the term “verified.” Simple Subscribe works on serverless infrastructure and uses an AWS Lambda to handle subscription, confirmation, and unsubscribe requests.
You can find the Simple Subscribe project, with its fully open-source code, on GitHub. I encourage you to pull up the code and follow along! In this post I’ll share each build step, the thought process behind the API’s single-responsibility functions, and security considerations for an AWS project like this one.
A non-verified email sign up process is straightforward. Someone puts their email into a box on your website, then that email goes into your database. However, if I’ve taught you anything about not trusting user input, the very idea of a non-verified sign up process should raise your hackles. Spam may be great when fried in a sandwich, but no fun when it’s running up your AWS bill.
While you can use a strategy like a CAPTCHA or puzzle for is-it-a-human verification, these can create enough friction to turn away your potential subscribers. Instead, a confirmation email can help to ensure both address correctness and user sentience.
To build a subscription flow with email confirmation, create single-responsibility functions that satisfy each logical step. Those are:
- Accept an email address and record it.
- Generate a token associated with that email address and record it.
- Send a confirmation email to that email address with the token.
- Accept a verification request that has both the email address and token.
To achieve each of these goals, Simple Subscribe uses the official AWS SDK for Go to interact with DynamoDB and SES.
At each stage, consider what the data looks like and how you store it. This can help to handle conundrums like, “What happens if someone tries to subscribe twice?” or even threat-modeling such as, “What if someone subscribes with an email they don’t own?”
Ready? Let’s break down each step and see how the magic happens.
The subscription process begins with a humble web form, like the one on my site’s main page. A form input with attributes
type="email" required helps with validation, thanks to the browser. When submitted, the form sends a GET request to the Simple Subscribe subscription endpoint.
Simple Subscribe receives a GET request to this endpoint with a query string containing the intended subscriber’s email. It then generates an
id value and adds both
id to your DynamoDB table.
The table item now looks like:
confirm column, which holds a boolean, indicates that the item is a subscription request that has not yet been confirmed. To verify an email address in the database, you’ll need to find the correct item and change
As you work with your data, consider the goal of each manipulation and how you might compare an incoming request to existing data.
For example, if someone made a subsequent subscription request for the same email address, how would you handle it? You might say, “Create a new line item with a new
id,” however, this might not be best strategy when your serverless application database is paid for by request volume.
Since DynamoDB Pricing depends on how much data you read and write to your tables, it’s advantageous to avoid piling on excess data.
With that in mind, it would be prudent to handle subscription requests for the same email by performing an update instead of adding a new line. Simple Subscribe actually uses the same function to either add or update a database item. This is typically referred to as, “update or insert.”
When a duplicate subscription request is received, the database item is matched on the
timestamp are overridden, which updates the existing database record and avoids flooding your table with duplicate requests.
After submitting the form, the intended subscriber then receives an email from SES containing a link. This link is built using the
id from the table, and takes the format:
In this set up, the
id is a UUID that acts as a secret token. It provides an identifier that you can match that is sufficiently complex and hard to guess. This approach deters people from subscribing with email addresses they don’t control.
Visiting the link sends a request to your verification endpoint with the
id in the query string. This time, it’s important to compare both the incoming
id values to the database record. This verifies that the recipient of the confirmation email is initiating the request.
The verification endpoint ensures that these values match an item in your database, then performs another update operation to set
true, and update the timestamp. The item now looks like:
You can now query your table to build your email list. Depending on your email sending solution, you might do this manually, with another Lambda, or even from the command line.
Since data for requested subscriptions (where
false) is stored in the table alongside confirmed subscriptions, it’s important to differentiate this data when querying for email addresses to send to. You’ll want to ensure you only return emails where
Similar to verifying an email address, Simple Subscribe uses
id as arguments to the function that deletes an item from your DynamoDB table in order to unsubscribe an email address. To allow people to remove themselves from your list, you’ll need to provide a URL in each email you send that includes their
id as a query string to the unsubscribe endpoint. It would look something like:
When the link is clicked, the query string is passed to the unsubscribe endpoint. If the provided
id match a database item, that item will be deleted.
Proving a method for your subscribers to automatically remove themselves from your list, without any human intervention necessary, is part of an ethical and respectful philosophy towards handling the data that’s been entrusted to you.
Once you decide to accept other people’s data, it becomes your responsibility to care for it. This is applicable to everything you build. For Simple Subscribe, it means maintaining the security of your database, and periodically pruning your table.
In order to avoid retaining email addresses where
false past a certain time frame, it would be a good idea to set up a cleaning function that runs on a regular schedule. This can be achieved manually, with an AWS Lambda function, or using the command line.
To clean up, find database items where
timestamp is older than a particular point in time. Depending on your use case and request volumes, the frequency at which you choose to clean up will vary.
Also depending on your use case, you may wish to keep backups of your data. If you are particularly concerned about data integrity, you can explore On-Demand Backup or Point-in-Time Recovery for DynamoDB.
Building your own subscriber list can be an empowering endeavor! Whether you intend to start a newsletter, send out notifications for new content, or want to create a community around your work, there’s nothing more personal or direct than an email from me to you.
I encourage you to start building your subscriber base with Simple Subscribe today! Like most of my work, it’s open source and free for your personal use. Dive into the code at the GitHub repository or learn more at SimpleSubscribe.org.