Building a barebones SaaS web app

JM

Joshua Mitchell / July 03, 2020

13 min read

Warning: Unfinished

Intro#

If you're reading this, I'll make a few assumptions about you:

  • You want to build a web app and make money off it.
  • You're either not technical at all, or maybe a little technical - but you're willing to learn.
  • You have an idea or 5, but hiring an expensive software engineer to build them is out of the question. You have no idea if they're going to work.
  • You're kind of interested in using No Code tools like webflow, zapier, etc, to stitch together something - but you're worried that, if it works, you'll run into No Code limitations and end up having to code the site anyway to add a particular feature. Or maybe you're not interested in No Code tools at all - you're pretty set on coding to begin with.

Hi! My name is Josh. I'm a software engineer, and I built something for you.

(For free, to be clear. You won't get halfway through this and run into a paywall.)

Here are the 2 things I want you to take from this post:

  1. A barebones web app up and running that
    • is simple enough that you understand the entire scope immediately (no complicated business logic to learn on top of everything)
    • is complex enough that it acts as a template for 99% of business ideas (i.e. for whatever feature you're trying to add, you already have a significant skeleton to bolt it onto)
  2. An understanding of what's going on from head to toe

The number one goal of this project, for me, is to start you off with killer momentum and make it last as long as possible. Even if the light at the end of the tunnel is far away, I still want you to see it.

I hate not knowing where to even begin. I hate the feeling of being stuck and having zero intuition for how to move forward. I have a closet full of projects I abandoned because I got stuck and lost motivation.

I want to do my best to prevent that for you.

To be clear, I will not even come close to teaching you all the programming knowledge you'll need to get your app to the point where it makes money. You will have to go elsewhere, and you'll probably have to build some stuff from scratch.

What I will give you is intuition. Any idea you have, I want you to immediately be able to go from the big picture (e.g. "I wish the site did x") straight down into the details ("Oh, I just have to add this structure to firebase, add this route, build this component, and that should be it!").

You'll have questions about the details (e.g. "shoot, how do I add a structure to firebase again?"), but you know exactly what to Google, and you have a reasonable idea what the answer looks like. :)

App Features#

The barebones web app we will build is a simple messaging app. Here's the entire list of features it'll have:

  • a sign up / login screen
  • a profile page that has a picture and a name
  • a page for sending messages
  • a page for checking your sent and received messages
  • a Stripe payment page that allows you to pay so you can send more than 1 message a day

That's it.

The idea is simple, but it's already packed with a whole bunch of functionality. In fact, it's intentionally over-engineered so that you can build on top of it with ease. The heavy lifting is already done:

  • authentication (using Firebase)
  • sending data (using React, Chakra-UI, and EmotionCSS for the front end)
  • receiving data (using Firebase's Firestore)
  • multiple pages (using Next.js)
  • accepting payments (using Stripe)

All built on top of Vercel for a delicious developer experience (i.e. easy deployments and configuration) and using GitHub for hosting and version control.

  • Want to add another page?
  • Want to make it into an eCommerce store?
  • Want to make a note taking app?

No prob. The infrastructure will already be there.

If all that tech I mentioned earlier sunds like Greek, don't worry about it. I'll explain it all step-by-step with copious amounts of context.

Oh - and the best part? It's all free. At least, at first. When you start getting traffic or sales, you'll start paying.

But, for now, ~weeee~!

App Sketch#

I used https://excalidraw.com/ to quickly mock up what it'll look like. You can use whatever mock up tool you want. Figma, Sketch, Adobe XD, or even just paper and pencil.

It's just five screens. You can

  • log in,
  • look at your profile,
  • check your messages,
  • send a message, and
  • pay to send more than 1 message.

That's it. Here's a preview:

Login:#

Profile:#

See All Messages:#

Send a Message:#

Pay to Send More than 1 Message:#

You could obviously do so much more:

  • implement deleting messages,
  • changing profile pictures,
  • etc.

But that's not the point - this template is for you to accelerate your business.

Building the Darned Thing#

Setting up Vercel#

(If you don't have a https://github.com account, create one.)

First, go to https://vercel.com/, create a Vercel account, and link your GitHub account to it.

Then, start a new project template. Choose Next.js for the template option.

If you've configured everything right, it'll automatically setsup your github project for you and deploy your site:

You can click "Visit" and a default site is deployed for you:

Now, clone the repository from your GitHub to your computer.

Then, install the packages locally that it comes with using e.g. yarn or npm install:

Finally, to run it locally: yarn dev

When you make changes, you can commit to master directly, but if you want a preview before shipping to production, Vercel offers automatic previews.

To see what I mean, make a change:

Then, create a new branch:

Now commit your change to that branch:

And push:

If you get a notification about it having no upstream branch, say yes.

Finally, go to github and click Compare & pull request:

And click Create pull request:

Since you linked the project to Vercel, it automatically creates a preview site based on this branch's changes:

Also, while we're in the project directory, we should set up prettier.

Make a file named .prettierrc.js in the base directory with these contents (or however you want to configure it):

    arrowParens: "always",
    singleQuote: true,
    tabWidth: 4,
    trailingComma: "none"
}

Firebase and Firebase Authentication#

Create an account at https://firebase.google.com/ if you don't already have one.

Go to the console and click Create Project:

Name it whatever you would like:

You'll see some options about Google Analytics - feel free to enable it if you wish.

Once you select those options, press continue, and your firebase instance will be deployed. Then, you'll end up on the main page:

Click the Add Firebase to your web app button:

Give it a nickname, and do not check Firebase Hosting (we are using Vercel for this).

You should end up at a screen that asks you to add the Firebase SDK to your web app:

Before we do anything, let's go back to our project and install firebase:

Now, let's set up our project to configure it. Make a new folder called lib:

And, in that folder, a file called firebase.js:

import * as firebase from 'firebase/app';
import 'firebase/auth';

if (!firebase.apps.length) {
    firebase.initializeApp({
        apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
        authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
        projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID
    });
}

export default firebase;

This is just some configuration that initializes firebase with all of your credentials.

What credentials, you ask? Well, remember that Firebase SDK screen you ended up on? We're going to create an environment variable configuration file.

Create a file named .env.local in the base directory.

Add your api key, your auth domain, and project ID from the firebase page in your browser:

to .env.local:

NEXT_PUBLIC_FIREBASE_API_KEY=AIz.. // put your full key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=saastemplate.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=saastemplate

Awesome. Now, firebase is technically setup. Let's add authentication:

First, go back to Firebase and click Authentication. Allow Google authentication in firebase:

Then, add an auth.js to the lib/ folder:

import React, { useState, useEffect, useContext, createContext } from 'react';
import firebase from './firebase';

const authContext = createContext();

export function AuthProvider({ children }) {
    const auth = useProvideAuth();
    return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}

export const useAuth = () => {
    return useContext(authContext);
};

function useProvideAuth() {
    const [user, setUser] = useState(null);

    const handleUser = (rawUser) => {
        if (rawUser) {
            const user = formatUser(rawUser);
            setUser(user);
            return user;
        } else {
            setUser(false);
            return false;
        }
    };

    const signInWithGoogle = () => {
        return firebase
            .auth()
            .signInWithPopup(new firebase.auth.GoogleAuthProvider())
            .then((response) => handleUser(response.user));
    };
    const signOut = () => {
        return firebase
            .auth()
            .signOut()
            .then(() => handleUser(false));
    };

    useEffect(() => {
        const unsubscribe = firebase
            .auth()
            .onAuthStateChanged((user) => handleUser(user));
        return () => unsubscribe();
    }, []);
    return {
        user,
        signInWithGoogle,
        signOut
    };
}

const formatUser = (user) => {
    return {
        uid: user.uid,
        email: user.email,
        name: user.displayName,
        photoUrl: user.photoURL
    };
};

Then, add an _app.js file to the pages/ folder:

import { AuthProvider } from '../lib/auth';

const App = ({ Component, pageProps }) => {
    return (
        <AuthProvider>
            <Component {...pageProps} />
        </AuthProvider>
    );
};

export default App;

Finally, modify your index.js file in the pages/ folder like so:

  1. Add the following import:
import { useAuth } from '../lib/auth';
  1. Change the Home function from
const Home = () => (
  // ...
);

to

const Home = () => {
  const auth = useAuth();
  return (
  // ...
  );
};
  1. Finally, add the following code somewhere in the <main> tag of the HTML:
<button onClick={(e) => auth.signInWithGoogle()}>
    Sign In
</button>
<div>{auth?.user?.email}</div>
{auth?.user && (<button onClick={(e) => auth.signOut()}>Sign Out</button>)}

Now, you should be able to log in with your gmail account using the Sign In button!

If you go to the Users tab of the Authentication page on Firebase, you can also see yourself as a user in the list of users:

You're going to want to add now.sh and your production domain to the "Authorized Domains" list so that your oauth setup works in production

Environment variables: You're going to have three sets of keys eventually:

  • One set for production (these keys will be stored as environment variables on Vercel)
  • One set for preview
  • One set for development (these keys will be stored locally on your machine)

The Database: Firebase Firestore#

We want to store user information. Hence, we're going to use Firebase's NoSQL database: Cloud Firestore

In the Firebase Console, click "Firestore" and then click "Create Database":

Select "Start in test mode" - this allows less strict data access rules (so that testing is easier). We will change this to production mode later.

Select the default location:

Awesome, we've activated our database. Now let's jump to the code to configure it. When we create a new user on the front end in "state", we also want to save that user in the database.

We're going to create a new file called db.js in the lib/ directory:

import firebase from './firebase';

const firestore = firebase.firestore();

export function createUser(uid, data) {
    return firestore
        .collection('users')
        .doc(uid)
        .set({ uid, ...data }, { merge: true });
}

Then add

createUser(user.uid, user);

after

const user = formatUser(rawUser);

in auth.js.

Finally, add

import 'firebase/firestore';

to your lib/firebase.js file.

Awesome - now logging in should save the user in the database!

Chakra-UI#

First, in the project, install the Chakra dependencies:

yarn add @chakra-ui/core @emotion/core @emotion/styled emotion-theming

Then, create a new styles folder in the base project.

Then, create a file called theme.js with the following contents in the styles/ directory:

import React from 'react';
import { theme as chakraTheme } from '@chakra-ui/core';

const theme = {
    ...chakraTheme,
    fonts: {
        body: `Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"`
    },
    fontWeights: {
        normal: 400,
        medium: 600,
        bold: 700
    }
};

export default theme;

Then, modify pages/_app.js to wrap the app in the ThemeProvider component and provide the theme:

import { ThemeProvider, CSSReset } from '@chakra-ui/core';
import { Global, css } from '@emotion/core';

import { AuthProvider } from '@/lib/auth';
import customTheme from '@/styles/theme';

const GlobalStyle = ({ children }) => {
    return (
        <>
            <CSSReset />
            <Global
                styles={css`
                    ::selection {
                        background-color: #47a3f3;
                        color: #fefefe;
                    }

                    html {
                        min-width: 360px;
                        scroll-behavior: smooth;
                    }

                    #__next {
                        display: flex;
                        flex-direction: column;
                        min-height: 100vh;
                    }
                `}
            />
            {children}
        </>
    );
};

const App = ({ Component, pageProps }) => {
    return (
        <ThemeProvider theme={customTheme}>
            <AuthProvider>
                <GlobalStyle />
                <Component {...pageProps} />
            </AuthProvider>
        </ThemeProvider>
    );
};

export default App;

Then, create a _document.js file in pages/ with the following contents:

import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
    render() {
        return (
            <Html>
                <Head>
                    <link rel="icon" href="/favicon.ico" />
                    <link
                        href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap"
                        rel="stylesheet"
                    />
                </Head>
                <body>
                    <Main />
                    <NextScript />
                </body>
            </Html>
        );
    }
}

export default MyDocument;

Finally, we're going to clean up pages/index.js a little bit.

import { useAuth } from '../lib/auth';
import { Button, Text, Heading, Code } from '@chakra-ui/core';
import Head from 'next/head';

const Home = () => {
    const auth = useAuth();
    return (
        <div className="container">
            <Head>
                <title>Simple Messaging App</title>
            </Head>
            <main>
                <Heading>Simple Messaging App!</Heading>
                <Text>
                    Current user:{' '}
                    <Code>{auth.user ? auth.user.email : 'None'}</Code>
                </Text>

                {auth.user ? (
                    <Button onClick={(e) => auth.signOut()}>Sign Out</Button>
                ) : (
                    <Button onClick={(e) => auth.signInWithGoogle()}>
                        Sign In
                    </Button>
                )}
            </main>
        </div>
    );
};

export default Home;

Configuring Absolute Imports:#

When this project gets bigger, we will start seeing imports that look like import blabla from '../../../etc' - so we're going to set up absolute imports and aliases.

Make a new file called jsconfig.json in the base directory:

{
    "compilerOptions": {
        "baseUrl": ".",
        "paths": {
            "@/components/*": ["components/*"],
            "@/lib/*": ["lib/*"],
            "@/styles/*": ["styles/*"]
        }
    }
}

To make sure it's working, replace

import { useAuth } from '../lib/auth';

with

import { useAuth } from '@/lib/auth'

in pages/index.js

This will require a server restart.

Finally, once you know it's working, refactor all the imports that use the components, lib, or styles directory.

You can do this quickly in Visual Studio Code on a Mac with CMD + SHIFT + F:

It will probably just be the _app.js file.


Discuss on Twitter