I built a (basic) Substack clone in a month

This was probably a waste of my time

Why we’re even talking about this

7 months ago, at Haneda Airport near Tokyo (this is not a relevant detail), I had an idea about starting a newsletter that explained software engineering to people who aren’t software engineers. I tweeted about it (surprise), there was some interest, and so I got started. 7 months later, we’re at around 4,000 free subscribers and $30K in annual recurring revenue (so thanks for that). If you’re wondering, I spent it all on sneakers. 

Anyway, this whole time, I’ve been running the newsletter through a tool called Substack - if you haven’t heard of it, god bless you. It takes care of everything from writing and hosting my posts, sending emails, and charging your precious credit cards. Substack was great for getting Technically off the ground, but at this point is really problematic:

  1. Substack takes 10% of my gross revenue

That’s the cost of the platform. If you’re running a free newsletter, you don’t need to care about that. But I’m on track to pay them almost $3K this year, which is a lot. And that’s before you take Stripe payment processing fees into account - my effective cost is closer to 15% on average. Legit Substack competitors like Pico charge half that.

  1. Substack is missing (really) important features

The Substack product is built for people who already have large audiences, and are trying to monetize them (like Bill Bishop, Matt Taibbi, Heather Cox Richardson, etc.). That’s why most of their top paid newsletters relate to politics, news, etc. For people like me, who are trying to build an audience from scratch, the product is really lacking. 

One of the (most egregious) examples is referrals - Substack doesn’t have any functionality for building a referral program, which is even weirder when you consider that referrals are generally the #1 channel for growing newsletters, and that this is probably 1-2 days of work for an average software engineer (although I’m told it’s being tested in beta!). Other examples of features I need but Substack doesn’t have - custom fonts, a custom domain, and better tracking.


So eventually I decided I would just build my own system for running my newsletter. How hard could it be! (famous last words). I’ve spent the past month or so building it, and it’s (basically) ready to launch.

One important thing to note: Substack is obviously not the only option out there for building and running a newsletter. Pico, Ghost, ConvertKit, Medium, Revue, and others are all solid options. My logic behind building my own was a bit convoluted :

  • I want to turn this into a proper piece of software beyond a newsletter (eventually), and a custom solution gives me that flexibility 

  • Building something in house gives me good content for the newsletter (in fact, you’re reading some of it as we speak)

  • I hadn’t built an app in a bunch of time and was getting kinda stale. Especially now that I’m investing more, it doesn’t hurt to be up to speed with React

So to be 100% clear - I am not recommending that you build your own Substack alternative, nor am I suggesting that there’s anything fundamentally wrong with Substack, nor am I saying that I don’t like them. So please, avoid quote tweeting this content and calling me a Substack hater.

How Substack works internally

Building a newsletter with a public facing site (e.g. a blog) has a bunch of elements on the tech side:

  1. A web app with authentication

  2. Payment processing for paid subscriptions

  3. A CMS for writing and managing content

  4. Sending emails to your subscribers

Let’s tackle them one by one.

  1. A web app with authentication

Substack.com is a website running on a server somewhere. It has functionality that lets visitors create accounts and log in, like I am currently here:

Once I’m authenticated (via password or email link), I can access Technically’s dashboard, send emails, add subscribers, etc. 

Authentication is generally considered a Very Annoying thing to deal with and build. At the most basic level, you can build a database that stores usernames and passwords - when a user signs up, their info gets added to that database, and when they try to sign in, their credentials get checked against what’s stored in the DB. If the username and password match, you let them in.

In practice, there’s a lot more to deal with - you don’t want users to have to login every time they load a new page or leave the site, so you’ll typically set a browser cookie with a token that says “this user is logged in!” After a day or so, you’ll set it to expire to keep things secure. You also need to build functionality that lets users log out.

Today, there are a ton of out of the box solutions for building authentication - the best known is probably Firebase, but Auth0 and Magic.link are good options too. I ended up choosing Supabase, but more on that later.

  1. Subscription management

Most of any newsletter’s subscribers will be free, not paid. Substack lets you collect those emails - they’re storing them in a basic database to use later for email (is my guess). 

Where things get more complex is paid subscribers. Substack relies on Stripe for payment processing - when you set up your newsletter, you need to connect your Stripe account or create one from scratch. This ended up being really clutch, because it made it extremely easy to take my paid subscribers and move them to my own platform. 

The way this works in practice is that when you sign up for a paid subscription, Substack runs your payment info through Stripe’s API and signs you up for the plan you chose (monthly, yearly, etc.). Stripe takes care of actually charging your card every month, dealing with fraud, etc.

Substack also takes care of upgrading subscriptions (from free to paid), cancelling subscriptions, and updating your credit card info. All of this is built on the Stripe API, but they needed to build the frontend (pages, forms, validation) that implements it.

  1. A CMS for writing and managing content

Substack gives you a UI for writing and publishing posts. It’s pretty similar to what you’d get in Google Docs. They’re taking all of the text you write, storing it in a database somewhere, and then publishing it on the web / sending it out to your email list when you click the appropriate buttons. This is what we’d call a Content Management System (CMS) in the blog / app world.

  1. Sending emails to your subscribers

Finally, with your subscribers stored in a nice database and your content written, you need some mechanism to actually send these emails (thousands of them every issue, to be exact). There are a bunch of third party vendors, like Sendgrid and Mailgun, but I’m guessing given the cost constraints, Substack probably built their own. 


There’s obviously a lot more to the Substack platform, but these are the basics you need to have a working newsletter with a web presence. So this is all of the stuff I needed to build on my own.

Building the Technically app

Here’s what the (semi) finished product looks like:

Back to the big pieces that make up Substack, let’s walk through how I tackled each:

  1. A web app with authentication

For the app, I chose a pretty standard stack for modern web apps: React on NextJS

  • React is a library for building componentized, interactive web apps

  • NextJS is a framework for building web apps in React

Infrastructure-wise, everything is running on Vercel, whose integration with NextJS is all-time-great in terms of developer experience (it’s the same people). The whole backend - authentication and storing user emails / plans - is running through Supabase.

Like Firebase, Supabase provides a really simple API for authentication. To sign up a new user, all I needed to do was build a basic form in React, and back it with Supabase’s couple-liner that creates a new user:

const {
  body: { user },
} = await supabase.auth.signup(
  'someone@email.com',
  'fOdaPdyTpkpxJgDVIORt'
)

Obviously in practice things are more complex than this, but you get the idea. A couple of relevant decisions I made:

→ Which auth provider to use

I settled on Supabase for auth, but it was not the first solution I tried. I started with Magic.link, which gets rid of the need for passwords entirely. The major issue is that it was way too expensive, and would end up costing a similar amount to Substack just for authentication, which would kind of defeat the entire purpose of building this app. I would also keep an eye out for Stytch when it’s ready for beta.

→ How to authenticate free subscribers

Nobody wants to enter a password when they’re signing up for a free newsletter, so I couldn’t run all users through a username / password form. The solution I settled on was a conditional form that only asks for a password if you’re signing up for a paid plan. If you’re signing up for a free plan, I drop the email in a database and note that the plan is free.

What that means is that free users don’t actually have a login - that’s only for paid users, which is kind of the whole point of the site (paywalling content) anyway. 

To handle upgrades, I built a basic upgrade flow. If you’re a free subscriber visiting the website, you can click on the “upgrade” link in the navbar:

That takes you to an upgrade form where you can choose a plan. When you enter your email, the app checks to see if your email already exists (i.e. you’re a free subscriber) - and if it doesn’t, you’ll need to sign up from scratch.

This is where you’ll create your password, and get added into the authentication store. Here’s what the app looks like when you’re logged in:

It’s not stunning but it’s a start 🙂

  1. Payment processing for paid subscriptions

Ah, payments, nobody’s favorite part of building an app. I outsourced basically this entire part to Stripe, more so than Substack has. Stripe provides two really clutch services that saved me a lot of time:

→ Stripe Checkout

Instead of building my own checkout page that handles credit card validation and all of that, Stripe has a hosted page you can use. When a user signs up for a paid plan or tries to upgrade, I just send them there. 

This saved me at least a day’s worth of engineering time. 

→ Stripe Portal

Instead of building my own billing management page where users can cancel and update their plans or update their credit card information, Stripe has a hosted page you can use. When logged in users click on the “manage billing” button in the navbar, they get redirected here:

For this page, you need to pass a Stripe Customer ID along when you create the portal session - for that, I built a basic endpoint that searches Stripe for the email of the currently logged in user.

This, too, saved me at least a day worth of engineering time. So thanks Stripe. Appreciate it.

  1. A CMS for writing and managing content

I actually prefer writing via Markdown, so that’s what I’m using for a CMS. The NextJS beginners course conveniently walks you through how to build a blog that gets compiled from Markdown pages, so I went through that and used the app that I ended up with as a base. So to create a new post, I just add a new Markdown file and that’s it. 

Part of the point of having a site is to create paywalled content that only paid subscribers can access. Each Markdown post has some head matter (stuff up top), and I added a boolean indicator for whether the post should be paywalled or not.

---

title: "Technically: APIs for the rest of us"

header_title: "APIs for the rest of us"

description: "A beginner's guide to APIs and making requests"

thumbnail: /images/apis/diagram.jpg

paywall: true

---

The Javascript that generates the post checks to see if it’s supposed to be paywalled, and then conditionally shows the content if you’re logged in (i.e. a paid subscriber).

Eventually I’m going to want to add the ability to preview the post (to tantalize visitors with my amazing content), and let people read a couple of paid posts before having to sign up a la digital newspapers.

  1. Sending emails to your subscribers

I’m using Sendgrid to take these posts from the web and send them out to my lovely subscribers on the days the bell tolls. Using the Sendgrid API is really easy (by design) - the hard part here is formatting email HTML

Now if, my dear reader, you’ve already had to deal with this, you understand what hellscapes I describe - and if not, I beg of you, don’t. Buy a template. Use a service. Hire someone. But do not, under any circumstances at all, try to write email HTML yourself.

It’s quite the nightmare. Right now, I’m using a frankenstein template I built from my existing HTML/CSS, and pasting in the post copy after it’s already rendered to HTML via NextJS. To inline the CSS (a must for consistent formatting across clients), I’m using this page, manually. So if you’re aware of anything better than this, please do let me know. 

Finally, to figure out who to send these emails to and actually trigger the messages, I built a Retool app (yea, I work here, whatever) that joins the data from my Supabase Postgres with the Sendgrid API to route posts. 

So I choose the audience I want to send the email to (free or paid), fill in the email HTML, and hit send. 

Where this goes next

So this is it - I’ve got the basic app working. The question that’s been figuratively tearing me apart over the past week is - do I actually use it? It’s ready, but running your own app comes with a lot of maintenance overhead. Right now, Substack deals with all of that for me - is saving 10% a year really worth it for me? Especially as someone who’s just doing this as a side project, I’m honestly not sure. The last thing I want is to be hanging out with the homies only to get paged and have to push a fix on my phone (idk, engineer noises).

They say that you don’t know what you got ‘till it’s gone, and this has definitely been the case with Substack. Building my own alternative has made me realize the depth of what I actually get from using Substack, but take for granted - the reality is that building all of that functionality would take me forever, and even the basics weren’t particularly easy.

So for now, I think I’m sticking with Substack. Technically has grown primarily via me writing (somewhat good) content, and that’s where my time should be focused while I have a head start. But at least I tried!