Building a SaaS in one week: How I built OnlineOrNot

Max Rozen

Max Rozen / March 06, 2021

When I first started building SaaS apps for side projects, it would take me a solid weekend solely to build the Stripe integration. These days, It's possible to build the whole SaaS app in one week.

Update: I've been running this business for over three years now, if you're interested to see how it's going, check out lessons from my third year running a SaaS.

Table of contents

Background Info

Let's start with some background about the app: OnlineOrNot is a pretty standard uptime checker - it does have some fancy logic to ensure the page is actually down before notifying you, but essentially it's a login/sign-up page, a dashboard, a "new page" form, a checker service, a transactional email integration, and a payment integration. I built a similar tool in 2018 (under the same name) to snapshot test GraphQL queries, but that project never took off.

I recently migrated my blog from Gatsby to Next.js, and that got me thinking - How much effort could it possibly be to use Next.js as the core of a SaaS?

I decided to do a quick test, and built a webpage to manually check if any URL is available, in a single afternoon (I later made it part of my landing page, before removing it entirely). I was impressed by how seamlessly Next.js blended together React, server-side rendering, and API routes - compared to my regular approach of using Gatsby.js as a landing page, create-react-app as the SaaS app, and a ton of AWS Lambda functions to handle the backend.

Tech Stack

Keeping in mind that I prefer to choose boring technology - I could have used DynamoDB/MongoDB to keep running costs as low as possible, but I opted for a relational data model.

Anyway, here's the tech stack:

  • Core app: Next.js, hosted on Vercel
  • UI/CSS framework: Tailwind CSS
  • Auth: NextAuth.js - a Next.js library
  • Backend: GraphQL + Node.js, written in TypeScript, sitting on a Next.js API route
  • Database: Postgres
  • Worker functions: Node.js/TypeScript, hosted on AWS Lambda
  • Payment integration: Stripe
  • Transactional Emails: Mailgun - I'd normally prefer to use Postmark, but didn't want to blocked from shipping by having to wait around to get verified
  • Newsletter Emails: Convertkit (referral link)

Approach

My work hours for my employer are 9am to 5pm, I tend to wake up at 7am, and go to bed around 11pm. If I gave 100% of my own time to this build, I would have had around 40 hours to work with.

In this particular week, I still either went to the gym, or ran each day, watched some Netflix, had dinner, and went out one night, so I had roughly 20 hours to work with.

Building the SaaS

First, a blog

To begin with, OnlineOrNot started its life as a blog. In particular, I cloned this example: blog-starter-typescript from the Next.js repo.

To get most of the blog functionality I wanted (sitemap, RSS feed, and code snippets in particular), I followed these steps.

Uptime Checker Test

With the blog running smoothly, I built an additional page to manually check uptime on my landing page (since removed).

To get technical with you, the form would submit data to a page living under status/[...url].tsx in my Next.js pages folder. Once it got a request, it sent off a request to an API route, which then sent the request to one of ten serverless functions deployed around the world (I had to use AWS Lambda for this part).

Seeing app-like functionality work so seamlessly on a blog is what inspired me to see how long it would take me to build a whole SaaS around the AWS Lambda function I wrote.

On Auth

For auth I initially wanted to use Passport, and try out Max Stoiber's recently released passport-magic-login, but I didn't enjoy the user experience of logging into an app with your email only, checking your email, clicking a link, then having to find the original tab to be able to use the app.

After a bit of searching, I came across NextAuth.js, which boasted similar functionality (and heaps more Auth Providers supported), with a bit more polish (to be fair, it's been around longer). It only took me a couple of hours to have both email-only login, and Google Auth0 login working.

Using Tailwind for UI

My typical approach to building UI rapidly would still be to use Bootstrap. In React, I opt for the Reactstrap library. It's never going to win me any design awards, but it helps me get the job done fast.

Since rewriting my own blog from Gatsby to Next.js, I've started writing my CSS using Tailwind utility classes. I find that whereas with Reactstrap I would use the default Bootstrap styles, then manually override each component, Tailwind has me thinking "How can I make this a component I can re-use?" more often, and it makes the code feel cleaner as a result.

To make my use of Tailwind CSS even faster, I paid for Tailwind UI, which has removed the "oh crap, where do I even start?" step for me when working on the frontend. It's kind of like paying for a designer to tell you "here's what screens like this tend to look like" - I highly recommend it.

On SSR and Emotion

One pitfall I noticed with server-side rendering (SSR) and Emotion, is that some of the Tailwind classes use the :first-child selector, and Emotion's SSR support (a CSS in JS library) extracts its CSS to be the first sibling of your component.

As a result, your pages tend to flicker on first load. The fix is simple enough - in Next.js you need to extract the critical styles into the head of the page (I followed the instructions here since I used twin.macro to use Tailwind with Emotion).

Forms!

I used to hate building forms in React until I came across React Hook Form.

Importing the hook, passing ref={register({ required: true })} to a few of my inputs, and implementing a submit handler is all it takes to build awesome forms in minutes these days (though it helps that I've spent a couple of years building forms in React).

OnlineOrNot Add Page Form

Once the user submits the form, they get to see their page being monitored:

OnlineOrNot Your Pages List

Latency troubles

Two days into building the SaaS with login/signup working, and it being possible to add pages to monitor, I decided to ship a v0.0.1 for friends to check out. I'm very glad I did, because I immediately noticed doing almost anything on the production build of the site took 3-6 seconds per click.

I eventually realised that the issue was caused by where I host my database. I typically host my Postgres database in AWS's ap-southeast-2 (Sydney, Australia) region, and Vercel hosts their functions somewhere on the US East Coast - probably in AWS's us-east-1 region.

So each request would hit their CDN, travel half-way across the world to the US, travel half-way across the world back to Sydney, Australia, and back to the US to finish the request.

Shutting down my Sydney database and spinning up a new one in AWS's us-east-1 region greatly reduced the latency issues.

Docs

Before launching, I knew I needed documentation - even though initially I was just building OnlineOrNot as a test to see how much effort is involved in building a SaaS in 2021, as a developer, nothing pisses me off more than crap/incomplete/missing documentation.

I followed the instructions at Docusaurus' homepage, spun up a quick site in Vercel, and mounted it onto the /docs path of OnlineOrNot - a pretty nifty Vercel feature that I loved from AWS CloudFront.

Stripe integration

Integrating Stripe's Checkout requires adding products in the Stripe dashboard, and following the docs - there are also examples to follow.

Since I implemented my last Stripe integration, Stripe launched Billing - greatly reducing the amount of effort required to let users upgrade, change, or cancel their subscriptions.

One of my employer's core values is "Don't f**k the customer", and I strongly identify with it, carrying it with me even when building side-projects. So I knew I had to have Stripe Billing to let my customers cancel at any time.

Rather than build the whole integration myself, I paid for Divjoy (referral link), which conveniently includes a Stripe integration (with Checkout, Billing, and a webhook endpoint). While I only wanted the Billing portion of the codebase, it was money well-spent - it gave me ideas to refactor the integration I built by following the Stripe docs.

Shipped!

With the Stripe integration built and tested, I deployed everything, and here we are - one week after I started toying around with a simple manual uptime checker in Next.js, I have a SaaS with login/sign-up, an app that tracks page uptime, and a way to pay for it (there's also a free tier).

Where to next?

Glad you asked!

I keep a changelog and a product roadmap tracking what's on the radar, what's coming soon, and what's released - I intend to continue shipping features for OnlineOrNot, and continue running it as part of my side-business.

Follow the Journey

Every month or so, I send an email with an update of how OnlineOrNot the business is going, subscribe below to receive it!

    You can unsubscribe at any time. Read the privacy policy.