SaaS, Web Development

How i Built a SaaS Application in 1 Week

March 13, 2024 • 9 min read

Let’s make a bet: you open your first social network at random, we bet that within the first 10 to 15 posts there is someone who is making the War of the Best Framework or Best Programming Language?

This is a bet from which I would follicly like to come out the loser, but just in case I was right, you can follow me on Twitter while you’re at it.

What are we talking about today? Let’s talk about how to shorten time, how to get tangible results quickly, and how to avoid wasting time unnecessarily.

We are going to do this by analysing a direct experience of mine, an app called Indeksu, which I developed in less than a week using the approach I am about to explain to you.

This application is proudly open-source, you can personally check the date between the first commit and the first release. Don’t be shy, take a look at the code, and let me know what you think. Leave a star for the support, or open an issue if you have any questions.

Let’s start with the basics.

If there is one thing that is imperative to keep in mind, it is to completely ignore any kind of fashion, trend or best framework of 2024. This is the shortcut to failure and wasted time.

Immediately trash any posts related to user-friendliness rankings, performance, or anything else. The truth is that there is no one framework that is better than another, but there the best solution for your context and your background.

If you are a painter who is used to painting with tempera, and your goal is to create a work in a tight timeframe, you are not going to experiment with oil, right? You will use what you know best, so that you can focus your energies on the end result, and not on the ‘How To’.

I hope the analogy is clear, having said that, from now on any mention of specific Tools or Frameworks will be made because relative to my context, and not because it is ‘better’ than another.

Goals and Objectives

The realisation of this application brought with it certain specific objectives, which guided the technical and design choices. First of all, the application had to be relatively simple to realise, so as not to require an excessive investment of time and resources.

However, due to its open-source nature it had to contain some less common use cases so as to be effective as a learning exercise for people interested in following the development process.

Given this personal need, and given the coinciding presence of various interesting use cases, I decided to develop Indeksu, an application enabling the automatic indexing of web pages in Google Search Console.

In detail, the application was to allow the indexing of web pages in Google Search Console in an automatic manner, without the need to manually access Google’s control panel. Moreover, it was to allow the management of multiple domains and the possibility of checking the indexation status of pages in real time.

The Choice of Framework

The choice of framework fell on Remix, a very basic meta-framework based on React. The reasons for the choice are soon stated:

  • Previous Knowledge: I have had the opportunity to use Remix and React in the past, so I could concentrate on the end result without having to worry about learning a new framework.
  • Copy and Paste: A lot of the logic related to the UI of components or certain functionalities such as authentication are often redundant for a SaaS business. Consequently, being able to copy and paste parts of code from previous projects was a big advantage.
  • Forms Management: Remix offers excellent forms management, I love its simplicity and effectiveness.

Let us now take a look at the various building blocks that make up the application, analysing with particular focus the choices that led to a higher development speed.

SQLite: Best Database Ever

Have you thought about how much time you waste setting up your PostgreSQL or MySQL? Install the server, create the database, configure the credentials, set the security policies, and so on.

Yes, of course I am deliberately blowing it out of proportion, but let me show you the procedure with SQLite:

touch database.sqlite

One command, one file, free, period. In my very personal opinion, this is the best solution for 90% of projects (even for production environments).

Unsure about the performances? Have a look at this statement from the official documentation:

SQLite works great as the database engine for most low to medium traffic websites (which is to say, most websites). The amount of web traffic that SQLite can handle depends on how heavily the website uses its database. Generally speaking, any site that gets fewer than 100K hits/day should work fine with SQLite.

The day you reach 100K hits/day, you will have the resources to migrate to a more complex solution, but until then, don’t waste time.

Prisma: A Necessary Compromise

Here is an example on a silver platter of the compromise one must make when it comes to technical choices.

The fact is, i don’t like Prisma that much. Moreover, I recently discovered Drizzle, an alternative that I find much lighter and more flexible. However, it would have been extremely silly of me to start experimenting with an ORM used in a project with such a tight schedule.

Having already used Prisma in previous projects, I could easily do some healthy copy and paste, to import the tables in common, and concentrate directly on the new tables needed for the application.

Below is a preview of the User model derived from a previous project and repurposed for Indeksu:

model User {
  id          String    @id @default(cuid())
  email       String    @unique
  password    String
  is_verified Boolean   @default(false)
  created_at  DateTime? @default(now())
  updated_at  DateTime? @updatedAt

  // Relationships
  OAuthToken OAuthToken?
  Sites      Site[]

  // I hate non-lowercased columns.
  @@map("users")
}

Authentication: Don’t Reinvent The Wheel

Authentication is one of the most tedious and repetitive parts of any application. Especially in Indie Hacking, where various projects are developed in a serial manner, it is essential to have a system that is easily reusable.

Having already tested it and used it in production, I relied on remix-auth an exceptional library, which solves a lot of problems with 70 lines.

In my specific case I used a strategy based on the email/password pair, but there are many other options available.

authenticator.use(
  new FormStrategy(async ({ context }) => {
    const data = context?.formData as FormData;

    if (!data) {
      throw new Error("Invalid email or password.");
    }

    const mail = data.get("email") as string;
    const password = data.get("password") as string;
    const action = data.get("action") as "SIGN_IN" | "SIGN_UP";

    if (!mail || !password) {
      throw new Error("Invalid email or password.");
    }

    if (action === "SIGN_UP") {
      // Apply validation with valibot
      const schema = safeParse(
        object({
          email: string([email()]),
          password: string([minLength(8)]),
        }),
        { email: mail, password }
      );

      if (!schema.success) {
        throw new Error("Invalid email or password.");
      }

      const exists = await db.user.findUnique({
        where: {
          email: schema.output.email,
        },
      });

      if (exists) {
        throw new Error("User already exists, please sign in instead.");
      }

      return db.user.create({
        data: {
          email: schema.output.email,
          password: await hash(schema.output.password, 10),
        },
      });
    }

    if (action === "SIGN_IN") {
      const exists = await db.user.findUnique({
        where: {
          email: mail,
        },
      });

      if (exists && exists.password) {
        const isPasswordValid = await compare(password, exists.password);

        if (isPasswordValid) {
          return exists;
        }

        throw new Error("Invalid email or password.");
      }

      throw new Error("Invalid email or password.");
    }

    throw new Error("Invalid action.");
  })
);

If you are interested, you can view the complete file here.

This configuration is literally a copy and paste from a previous project. If I had used a new framework, or a new library, I would have wasted precious time understanding how it works, and configuring it.

UI: Let’s Leave The Art To Michelangelo

Now, take a deep breath, empty your mind and prepare to fill it with an absolute truth:

Ugly but functional is better than beautiful but useless.

Let’s face it, if your app has 5 buttons two panels and 3 tables, don’t go down the rabbit hole by installing complex component libraries, or worse, by designing from scratch.

Once again, the fundamental rule applies: You already have something ready? Use it. In my case (now a routine) I did guess what? Copy and paste from a previous project, in which I had created UI components using TailwindCSS, and consequently reused those, with a few minor modifications.

import { forwardRef } from "react";
import { VariantProps, tv } from "tailwind-variants";

const styles = tv({
  base: "text-sm bg-opacity-10 p-5 leading-relaxed rounded-xl",
  variants: {
    kind: {
      error: "text-red-500 bg-red-500 bg-opacity-10",
      success: "text-green-500 bg-green-500 bg-opacity-10",
      warning: "text-yellow-500 bg-yellow-500 bg-opacity-10",
      info: "text-blue-500 bg-blue-500 bg-opacity-10",
    },
  },
  defaultVariants: {
    kind: "info",
  },
});

export interface AlertProps
  extends React.HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof styles> {}

const Alert = forwardRef<HTMLDivElement, AlertProps>(
  ({ className, kind, ...props }, ref) => {
    return (
      <div
        role="alert"
        ref={ref}
        className={styles({ class: className, kind })}
        {...props}
      />
    );
  }
);

Alert.displayName = "Alert";

export { Alert };

Extra Tip: Exploit Past Work

Whenever you come to a result that you are satisfied with, save it. Save the component, the function, the file, and reuse it. Maybe you will need it in the future, and it will save you time.

My UI components, they are pretty much the same as they have always been, with minor modifications here and there, I invested time to create them, but now they are saving me hours of work.

Mind you, I said invested because I reuse them, if I had created them and then left them there, I would have wasted the time.

Starting from scratch: What do I do?

As you have no doubt noticed from reading this article, speed of development is associated with one key factor, experience and continuous repetition of applying concepts and using tools.

Don’t worry if you’re a beginner, it’s almost obvious that you won’t be able to copy and paste from previous work, as you most likely won’t have any, but the good news is that you can start from scratch on the right foot, i.e. with the knowledge of what you need and what you don’t.

The key in this area is to fail often and quickly, so that you understand what works and what doesn’t, and above all, what works for you. There is no one-size-fits-all solution, mine is personal, and it may not work for you. You just have to put your hands on the keyboard and try.