I’m currently working on a huge project. Since I’d like to finish it soon, I’m instead going to spend an evening implementing a bespoke newsletter service.
A couple of months ago, I added a prominent RSS button to the overview of this blog, acquiescing to the venture capital overlords that line my pockets on the promise of a dedicated audience willing to trudge through inane jokes like this one.
Don’t see it?
Ah! Well, nobody’s going to click that. You know what people love? Newsletters1.
A newsletter feels like it should be pretty straightforward to scrap up from spare bolts and some crackers I found in the back of the cabinet. Roughly, our tech stack will look like this:
A backend that allows users to subscribe and unsubscribe, stores email addresses, and allows me to send emails.
A service that’ll handle the complexities of sending emails.
I’m not going for anything fancy here - ideally, this project shouldn’t take more than a few hours. Therefore, I’m going to reach for tech that I’ve used in the past:
Most of this backend will be publicly reachable; I want anyone to be able to sign up to the newsletter, or to unsubscribe. However, I’ll need gated access for the ability to send a newsletter. My go-to authentication infrastructure is a little too work-adjacent to blithely reuse, so I’ll keep things simple with some minimum-viable authentication: I’ve generated a secret string representing an API key, and the page to send out a newsletter to the email list validates against it.
classSendNewsletterCredentials(CredentialsBackedByFile):_CREDENTIALS_FILE=Path(__file__).parents/"axleos-blog-newsletter-internal-api-key.json"api_key:strclassSendNewsletterResource:defon_post(self,request:falcon.Request,response:falcon.Response)->None:authorized_api_key=SendNewsletterCredentials.get_default()newsletter_request=parse_json_body(request,SendNewsletterRequest)ifnewsletter_request.api_key!=authorized_api_key:raisefalcon.HTTPForbidden(description="Invalid API key")
There are a few abuse scenarios and failure cases that immediately come to mind before setting down any code.
One is that a malicious user might spam me with thousands of invalid email addresses. While I would never stand in the way of someone carrying out a personal vendetta, I will preemptively lay some groundwork for basic mitigations to help detect and manage this sort of thing. When someone signs up, I’ll store three pieces of information in Firestore:
The email address they submitted.
The IP address they submitted it from.
The date they signed up.
Attempting to detect the validity of an email address is so thorny that it’s the butt of jokes. Obviously, I’m not going to worry about it, and will outsource validation to Neutrino2. One risk here is that an attacker will use my API as a free way to access Neutrino’s non-free email address validation API. However, this method would require the attacker to sign folks up to my newsletter, so I’d say this is symbiotic3.
Another scenario that comes to mind is an attacker unsubscribing a different user from the newsletter. I won’t really try to prevent this. Instead, I’ll just send people an email confirming that they’ve been unsubscribed, so they know what’s up.
Lastly, I don’t want to (explicitly) leak who’s already signed up. Therefore, any error messages should be somewhat opaque4.
While implementing this, I had trouble accessing SendGrid.
Turns out they had an outage at the exact time I was trying to sign up.
In the end, they sorted themselves out before I finished setting up AWS SES.
As a last touch, I tried to make the emails look similar to the posts on this blog. I used similar CSS styling, and used the same motif in which the background of the main content uses randomly generated pastels. Each time you receive an email you’ll see a new color!
I didn’t worry about anything fancy like automatically generating and dispatching a newsletter as soon as I publish a new post. Instead, I’ll manually paste some details about the post whenever I’ve got a new one ready. Here’s what a notification email looks like:
While building this service, I was struck by the number of things I needed to bother setting up:
Project boilerplate (deployment metadata including an app.yaml and Dockerfile for GAE).
Enabling various cloud resources (such as GAE and Firestore).
Setting up automated deployments.
Setting up billing for GCP, SendGrid, and Neutrino.
Setting up an authentication scheme.
Managing deployment secrets and service secrets.
I was also pleased with things that I very much did not have to worry about:
Administering a database.
Administering specifics of the hosted instance.
This app is open source, and the App Engine instance is hosted here. Feel welcome to give it a whirl in the box below!
The idea of adding newsletter signup to this blog was suggested by my coworker Swapnil. Thanks, Swapnil! ↩︎
Against my better judgement I’m explicitly calling this out as a joke - I don’t want to send you emails if you don’t want to receive them! ↩︎
If someone was motivated, I think they could still glean whether a given email address is subscribed. This web-app is quite simple, and it’ll respond more quickly if it doesn’t need to do the work of sending out a “welcome!” email. Therefore, the response when hitting /subscribe is probably observably faster for subscribed users than unsubscribed users. One way to deal with this is to insert a variable delay, such that this endpoint always takes 3 seconds to respond. Obviously, I’m not going to worry about it. ↩︎