March 7, 20255 min read

🛠️ Building a SaaS with Next.js – Part 1: Project Setup & Authentication

#React#JavaScript#WebDev

This is the first part of a multi-step guide on building a SaaS application using Next.js. In this part, we'll cover:

Setting up a Next.js project with Bun
Configuring a PostgreSQL database with Prisma
Adding authentication using Auth.js with GitHub OAuth
Creating a simple sign-in and sign-out UI

By the end of this part, you'll have a fully functional authentication system ready to integrate with other SaaS features like payments, subscriptions, and user dashboards.


🚀 Step 1: Initialize a Next.js Project

Let’s kick things off by creating a new Next.js app:

bunx create-next-app@latest saas && cd saas

This sets up the project and moves you into the newly created saas directory.


📦 Step 2: Install Essential Dependencies

1️⃣ Add Prisma (Database ORM)

Prisma helps us interact with the database effortlessly. Install it with:

bun add prisma

Then, initialize Prisma:

bunx prisma init

This generates a prisma directory, including:

  • schema.prisma – Defines the database structure.
  • .env – Stores your database connection string.

2️⃣ Add ShadCN (UI Components)

ShadCN provides pre-styled components that work great with Tailwind CSS:

bunx --bun shadcn@latest init


3️⃣ Install Lucide Icons

Lucide is an icon library that works well with ShadCN:

bun add lucide-react

Now, you have a UI setup ready to roll. Let's move on to the database.


🗄️ Step 3: Configure the Database

Update the .env file to set up your database connection:

DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

Define Your Prisma Schema

Now, modify prisma/schema.prisma to define your user authentication schema:

enum SubscriptionStatus {
    Active
    Canceled
}

model User {
    id            String          @id @default(cuid())
    name          String?
    email         String          @unique
    emailVerified DateTime?
    image         String?
    accounts      Account[]
    sessions      Session[]
    Authenticator Authenticator[] // Optional for WebAuthn support

    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
}

model Account {
    userId            String
    type              String
    provider          String
    providerAccountId String
    refresh_token     String?
    access_token      String?
    expires_at        Int?
    token_type        String?
    scope             String?
    id_token          String?
    session_state     String?

    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt

    user User @relation(fields: [userId], references: [id], onDelete: Cascade)

    @@id([provider, providerAccountId])
}

model Session {
    sessionToken String   @unique
    userId       String
    expires      DateTime
    user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)

    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt
}

model VerificationToken {
    identifier String
    token      String
    expires    DateTime

    @@id([identifier, token])
}

model Authenticator {
    credentialID         String  @unique
    userId               String
    providerAccountId    String
    credentialPublicKey  String
    counter              Int
    credentialDeviceType String
    credentialBackedUp   Boolean
    transports           String?

    user User @relation(fields: [userId], references: [id], onDelete: Cascade)

    @@id([userId, credentialID])
}

Apply the schema changes:

bunx prisma migrate dev
bunx prisma generate

Now, let’s integrate authentication!


🔐 Step 4: Set Up Authentication with Auth.js

Auth.js (formerly NextAuth) makes adding authentication super simple.

First, install Auth.js:

bun add next-auth@beta

Generate an Encryption Key

bunx auth secret

Copy the generated key and store it in .env under AUTH_SECRET.


Create the Auth.js Configuration

Add an auth.ts file inside src/:

./src/auth.ts

import NextAuth from "next-auth"
 
export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [],
})

This sets up the auth configuration but doesn’t have any providers yet. We'll add GitHub authentication next.


🔑 Step 5: Add GitHub OAuth Provider

If you haven't set up GitHub OAuth before, follow this guide.

Add GitHub OAuth Credentials

Go to GitHub Developer SettingsOAuth Apps, create a new app, and get:

  • Client ID
  • Client Secret

Then, update your .env file:

AUTH_GITHUB_ID="your_github_client_id"
AUTH_GITHUB_SECRET="your_github_client_secret"

Register GitHub in Auth.js

Modify auth.ts to include GitHub as an authentication provider:

import NextAuth from "next-auth"
import GitHub from "next-auth/providers/github"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [GitHub],
})


🏛️ Step 6: Connect Prisma with Auth.js

Since we’re using a database, we need to integrate Prisma with Auth.js.

Install the Prisma Adapter

bun add @prisma/client @auth/prisma-adapter

Create a Global Prisma Client

./src/prisma.ts

import { PrismaClient } from "@prisma/client"
 
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
export const prisma = globalForPrisma.prisma || new PrismaClient()

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma

Use Prisma in Auth.js

Modify auth.ts to add the Prisma adapter:

import NextAuth from "next-auth"
import GitHub from "next-auth/providers/github"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/prisma"

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [GitHub],
  adapter: PrismaAdapter(prisma),
})

Apply the schema and generate Prisma client:

bunx prisma migrate dev
bunx prisma generate


👤 Step 7: Add Sign-In and Sign-Out Buttons

To test authentication, create a simple sign-in/sign-out UI in ./src/app/page.tsx:

import { auth, signIn, signOut } from "@/auth.ts";

function SignInButton() {
    return <form action={async () => {
	    "use server";
	    await signIn()
    }}>
	    <button type="submit">Sign In</button>
    </form>
}

function SignOutButton() {
	return <form action={async () => {
	    "use server";
	    await signOut()
    }}>
	    <button type="submit">Sign Out</button>
    </form>
}

export default async function Page() {
	const session = await auth();

	return <main>
		{!session?.user
			? <SignInButton />
			: <SignOutButton />
		}
	</main>
}


🎯 What’s Next?

This first part of our SaaS guide covered:

Project setup with Next.js
Database setup with Prisma
Authentication with Auth.js & GitHub OAuth

Next up in Part 2: Handling user roles, Stripe payments, and subscription logic!

👉 Stay tuned for the next part, where we'll integrate Stripe for payments and subscription management! 🚀