Making a newsletter backend
Reading time: 4 minutes
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? Newsletters✱.
✱ Note
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.
- Some JavaScript on this site that’ll ask the backend to sign up a new user.
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:
- Google App Engine for hosting the backend.
- Falcon and gunicorn for the backend’s web stack.
- Firestore for storing subscribed users.
- SendGrid for sending emails.
- jinja2 for rendering HTML templates.
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.
newsletter/api/send_newsletter.py
class SendNewsletterCredentials(CredentialsBackedByFile):
_CREDENTIALS_FILE = Path(__file__).parents[2] / "axleos-blog-newsletter-internal-api-key.json"
api_key: str
class SendNewsletterResource:
def on_post(self, request: falcon.Request, response: falcon.Response) -> None:
authorized_api_key = SendNewsletterCredentials.get_default()
newsletter_request = parse_json_body(request, SendNewsletterRequest)
if newsletter_request.api_key != authorized_api_key:
raise falcon.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 Neutrino✱. 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 symbiotic✱.
✱ Note
✱ Note
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 opaque✱.
✱ Note
/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.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
andDockerfile
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.
- Managing dependencies.
I was also pleased with things that I very much did not have to worry about:
- Administering a database.
- Managing SSL.
- 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!