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.
Introducing Simple Subscribe
If you’re interested in managing your own mailing list or newsletter, you can set up Simple Subscribe on your own AWS resources to collect email addresses. This open source API is written in Go, and runs on AWS Lambda. Visitors to your site can sign up to your list, which 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.
Building a verified subscription flow
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.
Subscribing
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 email
and id
to your DynamoDB table.
The table item now looks like:
confirm | id | timestamp | |
---|---|---|---|
subscriber@example.com |
false | uuid-xxxxx |
2020-11-01 00:27:39 |
The 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 confirm
to true
.
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.”
In a database like SQLite this is accomplished with the UPSERT syntax. In the case of DynamoDB, you use an update operation. For the Go SDK, its syntax is UpdateItem
.
When a duplicate subscription request is received, the database item is matched on the email
only. If an existing line item is found, its id
and timestamp
are overridden, which updates the existing database record and avoids flooding your table with duplicate requests.
Verifying email addresses
After submitting the form, the intended subscriber then receives an email from SES containing a link. This link is built using the email
and id
from the table, and takes the format:
<BASE_URL><VERIFY_PATH>/?email=subscriber@example.com&id=uuid-xxxxx
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 email
and id
in the query string. This time, it’s important to compare both the incoming email
and 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 confirm
to true
, and update the timestamp. The item now looks like:
confirm | id | timestamp | |
---|---|---|---|
subscriber@example.com |
true | uuid-xxxxx |
2020-11-01 00:37:39 |
Querying for emails
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 confirm
is 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 confirm
is true
.
Providing unsubscribe links
Similar to verifying an email address, Simple Subscribe uses email
and 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 email
and id
as a query string to the unsubscribe endpoint. It would look something like:
<BASE_URL><UNSUBSCRIBE_PATH>/?email=subscriber@example.com&id=uuid-xxxxx
When the link is clicked, the query string is passed to the unsubscribe endpoint. If the provided email
and 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.
Caring for your data
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 confirm
is 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 confirm
is false
and 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.
Build your independent subscriber base
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.
api aws coding go data cybersecurity websites