In this blog post, we'll explore how to set up OAuth authentication in a modern web application using a powerful stack that ensures efficiency, scalability, and developer productivity. By the end, you'll have a working OAuth system integrated into a full-stack application.
Tech Stack:
- Better_Auth v1: A lightweight and extensible authentication library for TypeScript.
- Next.js: A React framework for building server-rendered web applications.
- Prisma: A modern ORM for database management.
- Shadcn: A utility-first component library.
- TailwindCSS: A popular utility-first CSS framework.
Prerequisites:
Before we begin, make sure you have the following tools installed:
- Node.js (LTS version)
- npm, yarn, or pnpm (package managers)
- A PostgreSQL database instance (local or hosted, such as Supabase or PlanetScale). I'll be using Docker for local development
- Basic knowledge of TypeScript, Next.js, and Prisma.
Step 1: Setting up your Next.js project
Create a New Project Directory:
Begin by creating a folder for your project and navigating into it via the terminal. Use the following commands:
mkdir better_auth_nextjs_oauth
cd better_auth_nextjs_oauth
Install Next.js in the Directory:
Initialize a new Next.js project within the folder using the command below. You can use your preferred package manager (e.g., npm, yarn, or pnpm). For this guide, we'll use pnpm.
pnpx create-next-app@latest .
💡Tip: If you don't have pnpm installed, visit pnpm.io and follow the instructions for your operating system.
Configure the Project Options:
Follow the prompts displayed in the terminal to configure your project. You will be asked to make selections such as enabling TypeScript, which is recommended for better type safety and development experience.
Use the arrow keys to navigate through the options and choose according to your preferences.
Step 2: Installing Dependencies
Install the necessary packages for the tech stack by running the following commands:
pnpm add better-auth@latest
pnpm add -D prisma
pnpm add @prisma/client
pnpm install react-icons --save
pnpm add oslo
To install shadcn-ui, a utility-first component library, run the following command:
pnpm dlx shadcn@latest init
This will set up shadcn-ui in your project, allowing you to start using its components right away.
When prompted, choose from the available options to customize your shadcn-ui setup. Follow the instructions in the terminal to select the components and configurations that best suit your project needs.
![Image description]](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x3l4jss12ft95ieuoef2.png)
To add reusable components from shadcn-ui, you can use the following command:
pnpx shadcn@latest add button card
Step 3: Setting Up the Database (Local or Cloud)
Option 1: Local Development with Docker:
💡Tip: If you're using a cloud-based database provider like Neon or PlanetScale, you can skip this step and proceed to configure your database connection directly through their provided URLs.
Create a docker-compose.yml
File
To set up a PostgreSQL database using Docker, create a docker-compose.yml
file in your project’s root directory. This file will define the configuration for your database container.
Here's an example configuration for the docker-compose.yml
file:
# docker-compose.yml
version: '3.9'
services:
db:
container_name: BETTER_AUTH_NEXTJS_OAUTH
image: postgres:17-alpine
restart: always
ports:
- 5432:5432
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_DB=OAUTH
volumes:
postgres-data:
This configuration sets up a PostgreSQL container with a persistent volume and environment variables for your database credentials.
Set Up Environment Variables for the Database
Create a .env
file in the root of your project and define the necessary environment variables for your database connection. Here's an example configuration:
//.env
POSTGRES_PASSWORD="your_password_here"
POSTGRES_USER="your_username_here"
DATABASE_URL="postgresql://your_username_here:your_password_here@localhost:5432/mydb?schema=public"
- Replace
your_password_here
andyour_username_here
with your own PostgreSQL credentials. -
DATABASE_URL
should point to your local database or a cloud database if you are using a provider.
Run the Docker Container
Once you’ve configured the docker-compose.yml
file and set up the environment variables, you can start the Docker container by running the following command:
docker compose up -d
This command will start the PostgreSQL container in detached mode (-d
), ensuring your database is up and running in the background. You can now proceed with the rest of the setup in your Next.js application.
If you ever need to stop the container, you can use:
docker compose down
Option 2: Using a Hosted Database
If you're using a cloud database provider (such as Neon, PlanetScale, or others), you can skip the local Docker setup. Instead, simply copy the provided Database URL from your cloud provider and add it to your .env
file.
For example:
# .env
DATABASE_URL="postgresql://username:password@host:port/database_name?sslmode=require"
Ensure that the DATABASE_URL
contains the correct connection details such as your:
username
password
-
host
(database server URL) -
port
(usually5432
for PostgreSQL) database_name
Once you've added the correct URL, your application will be able to connect to the hosted database seamlessly.
Step 4: Setting Up Prisma
Initialize Prisma CLI
This will create a new prisma
folder in your project, containing the schema.prisma
file and configuration for Prisma.
pnpm dlx prisma init
This will create a new prisma
folder in your project, containing the schema.prisma
file and configuration for Prisma.
Configure the Database Connection:
Open the prisma/schema.prisma
file and ensure that the DATABASE_URL
points to your PostgreSQL database. It should look something like this:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
Prisma will automatically read the DATABASE_URL
from your .env
file.
Create a Database Folder
Inside the src
directory of your project, create a folder named db
. This will house all database-related files.
Create a Database Client File
Inside the db
folder, create a file named index.ts
. Add the following code to configure and export a singleton instance of the Prisma Client:
// src/db/index.ts
import { PrismaClient } from '@prisma/client'
const prismaClientSingleton = () => {
return new PrismaClient()
}
declare const globalThis: {
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;
const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()
export default prisma
if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma
Create Schema for auth
In this step, you'll define a database schema to manage authentication-related data, such as users, sessions, accounts, and verification tokens.
- Open the
prisma/schema.prisma
file in your project. - Replace its contents with the following schema:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
name String
email String
emailVerified Boolean @default(false)
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
twoFactorEnabled Boolean @default(false)
Session Session[]
Account Account[]
@@unique([email])
@@map("user")
}
model Session {
id String @id @default(cuid())
expiresAt DateTime
token String @unique
ipAddress String?
userAgent String?
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("session")
}
model Account {
id String @id @default(cuid())
accountId String
providerId String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
accessToken String?
refreshToken String?
idToken String?
expiresAt DateTime?
password String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
scope String?
@@map("account")
}
model Verification {
id String @id @default(cuid())
identifier String
value String
expiresAt DateTime
createdAt DateTime? @default(now())
updatedAt DateTime? @updatedAt
@@map("verification")
}
Explanation of the Schema
-
User: Contains basic user information and manages relationships to
Account
andSession
models. - Session: Tracks user sessions, including tokens, IP addresses, and user agents.
- Account: Manages linked OAuth accounts for third-party login providers such as Google or GitHub.
- Verification: Handles email or token-based verification (e.g., passwordless login or two-factor authentication).
Generate and Migrate the Prisma Schema
After defining your models in the schema.prisma
file, you need to generate the Prisma client and apply the database migrations.
-
Generate Prisma Client
First, generate the Prisma Client, which provides an API to interact with your database:
pnpx prisma generate
This will generate the necessary client files for accessing the database from your application.
- Run Migrations
Apply the migrations to your database by running the following command:
pnpx prisma migrate dev --name auth
This will apply the migration to your database and generate the required tables (e.g., for User
, Session
, Account
, etc.).
Once this setup is complete, Prisma will be ready to use for querying your database in your application.
Step-4: Integrate Better_Auth
Setting Up Environment Variables:
Add the following configuration to your .env
file to provide the necessary environment variables for authentication:
# General authentication settings
BETTER_AUTH_SECRET="your-secret-key" # Replace with your own secure key or generate one using "pnpm dlx auth secret"
BETTER_AUTH_URL="http://localhost:3000" # Base URL of your app during local development
NEXT_PUBLIC_APP_URL="http://localhost:3000" # Public-facing base URL of your app
# Google OAuth credentials
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
# GitHub OAuth credentials
GITHUB_CLIENT_ID="your-github-client-id"
GITHUB_CLIENT_SECRET="your-github-client-secret"
Explanation:
-
BETTER_AUTH_SECRET: A secret key used by Better_Auth for signing JWTs. You can generate one using
pnpm dlx auth secret
. - BETTER_AUTH_URL: The base URL for your application (used for redirecting after authentication).
- Google and GitHub OAuth credentials: Replace the placeholders with the actual Client ID and Client Secret you obtain from Google and GitHub developer portals. Ensure the redirect URIs match the paths configured in your OAuth provider’s settings.
- NEXT_PUBLIC_APP_URL: The public URL of your app, which is used for client-side requests.
Notes on Obtaining OAuth Credentials:
For Google OAuth
- Go to the Google Cloud Console.
- Navigate to Credentials > Create Credentials > OAuth 2.0 Client IDs.
-
Under Authorized Redirect URIs, set the following:
- For local development:
http://localhost:3000/api/auth/callback/google
- For production: Use your application's domain, e.g.,
https://example.com/api/auth/callback/google
.
- For local development:
For GitHub OAuth
- Visit the GitHub Developer Settings.
- Create a new OAuth App under Developer Applications.
-
Set the following redirect URIs:
- For local development:
http://localhost:3000/api/auth/callback/github
- For production: Use your application's domain, e.g.,
https://example.com/api/auth/callback/github
.
- For local development:
💡### Why These Redirect URIs Matter
The redirect URIs are critical for OAuth authentication. They specify the exact location where the OAuth provider (Google or GitHub) should send users after they authenticate. Make sure the URIs you provide in the OAuth configuration match exactly with those used in your application; otherwise, the authentication process will fail.
Setting Up the Authentication Configuration:
-
Create the
auth.ts
FileInside the
lib
folder of your project, create a file namedauth.ts
. This file will contain the configuration for Better_Auth. -
Add the Following Code
Copy and paste the code below into the
auth.ts
file:
// auth.ts
import prisma from '@/db';
import {
betterAuth
} from 'better-auth';
import { prismaAdapter } from 'better-auth/adapters/prisma';
export const auth = betterAuth({
appName: "better_auth_nextjs",
database: prismaAdapter(prisma, {
provider: "postgresql"
}),
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
redirectURI: process.env.BETTER_AUTH_URL + "/api/auth/callback/google",
},
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
redirectURI: process.env.BETTER_AUTH_URL + "/api/auth/callback/github",
}
},
});
Explanation of the Code:
-
betterAuth
: Initializes the authentication library with the following parameters:-
appName
: Specifies the name of your application. -
database
: Connects the Prisma adapter to the PostgreSQL database defined in your.env
file. -
socialProviders
: Configures Google and GitHub OAuth providers.
-
-
Dynamic Redirect URIs:
- The
redirectURI
dynamically appends the callback path to the base URL (BETTER_AUTH_URL
) defined in your environment variables. This ensures compatibility with both local and production environments.
- The
Setting Up the Authentication Client
-
Create the
auth-client.ts
FileInside the
lib
folder, create a new file namedauth-client.ts
. This file will manage client-side authentication interactions. -
Add the Following Code
Paste the code below into the newly created file:
// src/lib/auth-client.ts
import {
createAuthClient
} from "better-auth/react";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL,
})
export const {
signIn,
signOut,
signUp,
useSession
} = authClient;
Explanation:
-
createAuthClient
:- This function initializes the client-side authentication logic using Better_Auth.
- The
baseURL
is sourced from theNEXT_PUBLIC_APP_URL
environment variable, ensuring compatibility with both local and production environments.
-
Exported Methods:
-
signIn
: Initiates the sign-in process for users. -
signOut
: Logs the user out. -
signUp
: Handles user registration. -
useSession
: Retrieves and manages the current user's authentication session.
💡 Why This File is Important
The
auth-client.ts
file abstracts client-side authentication functions, making them reusable and straightforward to integrate into React components. By centralizing the authentication logic, you maintain cleaner code and enhance the maintainability of your project. -
Creating an API Route Handler for Authentication
To handle authentication API requests, you'll need to set up a route handler in your application. This will process all requests directed to the /api/auth/*
endpoint (or any custom base path you configure).
-
Create the Route Handler
In your framework's designated directory for API routes (e.g.,
/app/api/auth
for Next.js), create a catch-all route handler to manage authentication requests. -
Add the Following Code
For a Next.js application, create a file named
route.ts
inside the/app/api/auth/[...all]
directory and paste the following code:
// /app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth"; // path to your auth file
import { toNextJsHandler } from "better-auth/next-js";
export const { POST, GET } = toNextJsHandler(auth);
Explanation:
-
toNextJsHandler
:- Converts the Better_Auth configuration into route handlers compatible with Next.js.
- Automatically sets up handlers for the HTTP methods (e.g.,
POST
andGET
) required for authentication.
-
Catch-All Route (
/api/auth/*
):- Ensures that all authentication-related requests (e.g., sign-in, callbacks) are routed through this handler.
- This centralizes your authentication logic for better maintainability.
💡 Why This Setup is Necessary
By creating this route handler:
- You streamline the handling of authentication-related API requests.
- The
/api/auth/*
endpoint acts as a dedicated entry point for Better_Auth to manage user sessions, social logins, and other auth operations.
Step 5: Create Sign-In Component
Create a Reusable CardWrapper
Component
To improve modularity and reusability, let's create a CardWrapper
component. This component will wrap content inside a styled card layout, making it easy to display various types of content consistently throughout the app.
Here’s the improved version of the CardWrapper
component:
import React from 'react'
import { Card, CardContent, CardDescription, cardHeader, CardTitle } from './ui/card'
import Link from 'next/link'
interface CardWrapperType {
children: React.ReactNode,
cardTitle: string,
cardDescription: string
}
const CardWrapper = ({
children,
cardTitle,
cardDescription,
}: CardWrapperType) => {
return (
<Card className="w-[400px] relative">
<CardHeader>
<CardTitle>{cardTitle}</CardTitle>
<CardDescription>{cardDescription}</CardDescription>
</CardHeader>
<CardContent>
{children}
</CardContent>
</Card>
)
}
export default CardWrapper
Explanation:
-
Component Structure:
-
children
: This is the content that will be rendered inside the card. -
cardTitle
: The title of the card, which will be prominently displayed at the top. -
cardDescription
: A brief description under the title that provides more context for the card.
-
-
Styling:
- The
Card
container has utility classes likew-full
,max-w-md
,mx-auto
,bg-white
,rounded-lg
, andshadow-md
to control the width, margin, background color, border radius, and shadow of the card. This ensures the card looks well-aligned, centered, and visually distinct. - Inside the card header, the
CardTitle
andCardDescription
have additional styling for text size, weight, and color to improve readability and contrast.
- The
-
Modularity:
- By separating the card layout into its own reusable component (
CardWrapper
), it becomes easy to reuse this structure throughout the app wherever you need a card with a title and description. This promotes consistency in the design and reduces code duplication.
- By separating the card layout into its own reusable component (
Create FormError
and FormSuccess
Components:
To improve error and success message handling in forms, let's create reusable components for displaying error and success messages.
-
FormError
Component:The `FormError` component will display error messages with an icon and a styled container to alert the user.
// app/components/form-error.tsx
import React from 'react'
import { AiFillExclamationCircle } from "react-icons/ai";
type FormErrorProps = {
message?: string
}
const FormError = ({message}: FormErrorProps) => {
if (!message) return null
return (
<div className="bg-destructive/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-destructive">
<AiFillExclamationCircle className='w-4 h-4'/>
{message}
</div>
)
}
export default FormError
Explanation:
-
Props:
-
message
: A string containing the error message that will be displayed. If no message is provided, the component returnsnull
.
-
-
Styling:
-
bg-red-100
: Light red background for the error message. -
text-red-600
: Red text color for visibility and to emphasize the error. -
p-3
: Padding inside the message box. -
rounded-md
: Rounded corners for a smooth appearance. -
flex items-center gap-x-2
: A flex container with horizontal spacing between the icon and the message text. - The
AiFillExclamationCircle
icon is used to represent the error visually.
-
FormSuccess
Component:
The FormSuccess
component is similar to the FormError
, but it's used for success messages.
// app/components/form-success.tsx
import React from 'react'
import { AiFillExclamationCircle } from "react-icons/ai";
type FormErrorProps = {
message?: string
}
const FormError = ({message}: FormErrorProps) => {
if (!message) return null
return (
<div className="bg-destructive/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-destructive">
<AiFillExclamationCircle className='w-4 h-4'/>
{message}
</div>
)
}
export default FormError
Explanation:
-
Props:
-
message
: A string containing the success message to be displayed. If no message is provided, the component returnsnull
.
-
-
Styling:
-
bg-green-100
: Light green background to indicate success. -
text-green-600
: Green text color to match the success color scheme. -
p-3
: Padding inside the message box for better spacing. -
rounded-md
: Rounded corners for a sleek and modern design. -
flex items-center gap-x-2
: Flexbox container with horizontal spacing between the success icon and message text. - The
AiFillCheckCircle
icon represents a successful action.
-
Create the sign-in.tsx
file
Inside the components/auth
folder, create a new file named sign-in.tsx
. This file will contain the component responsible for rendering the sign-in UI and handling the authentication logic.
Here's how to structure your component:
// components/auth/sign-in.tsx
"use client"
import React, { useEffect, useState } from 'react'
import { Button } from '../ui/button'
import CardWrapper from '../card-wrapper'import { FaGithub } from 'react-icons/fa'
import { FcGoogle } from 'react-icons/fc'
import { signIn } from '@/lib/auth-client'
import FormError from '../form-error'
import { FormSuccess } from '../form-success'
import { useRouter } from 'next/navigation'
const SignIn = () => {
const router = useRouter()
const [error, setError] = useState("")
const [success, setSuccess] = useState("")
const [loading, setLoading] = useState(false)
useEffect(() => {
setError("")
setSuccess("")
setLoading(false)
}, [])
const githubSignIn = async () => {
try {
await signIn.social({
provider: "github",
callbackURL: "/"
}, {
onResponse: () => {
setLoading(false)
},
onRequest: () => {
setSuccess("")
setError("")
setLoading(true)
},
onSuccess: () => {
setSuccess("Your are loggedIn successfully")
},
onError: (ctx) => {
setError(ctx.error.message)
}
})
} catch (error: unknown) {
console.log(error)
setError("Something went wrong")
}
}
const googleSignIn = async () => {
try {
await signIn.social({
provider: "google"
}, {
onResponse: () => {
setLoading(false)
},
onRequest: () => {
setSuccess("")
setError("")
setLoading(true)
},
onSuccess: () => {
setSuccess("Your are loggedIn successfully")
router.replace('/')
},
onError: (ctx) => {
setError(ctx.error.message)
}
})
} catch (error: unknown) {
console.error(error)
setError("Something went wrong")
}
}
return (
<CardWrapper
cardTitle='Sign In'
cardDescription='Enter your email below to login to your account'
>
<div className='flex gap-y-2 flex-col'>
<FormError message={error} />
<FormSuccess message={success} />
<Button
variant={"outline"}
onClick={githubSignIn}
disabled={loading}
>
<FaGithub />
Sign in With Github
</Button>
<Button
variant={"outline"}
onClick={googleSignIn}
disabled={loading}
>
<FcGoogle />
Sign in with Google
</Button>
</div>
</CardWrapper>
)
}
export default SignIn
Explanation:
-
State management: The component uses
useState
for managing error, success, and loading states. -
OAuth sign-in: The
githubSignIn
andgoogleSignIn
functions handle authentication with GitHub and Google respectively. They use thesignIn.social
method from theauth-client.ts
file. -
Form feedback:
FormError
andFormSuccess
components display any errors or success messages during the sign-in process. -
UI: The
Button
component is used to create sign-in buttons for GitHub and Google. The buttons are disabled while the sign-in process is in progress
This component will handle the authentication process and provide users with an interface to sign in using their Google or GitHub accounts.
Create the sign-in
folder
Inside the app
folder, create a new folder named sign-in
. This folder will contain the page and components related to the sign-in process.
Create the page.tsx
file
Inside the sign-in
folder, create a new file named page.tsx
. This will be the main page that renders the sign-in component and handles the routing logic
Here's how to structure the page.tsx
file:
// /app/signin/page.tsx
import SignIn from '@/components/auth/sign-in'
import React from 'react'
const SignInPage = () => {
return (
<SignIn />
)
}
export default SignInPage
Explanation:
-
Component Import: The
SignIn
component fromcomponents/auth/sign-in
is imported and used to display the sign-in form. -
Styling: The
min-h-screen
,bg-gray-50
,flex
,justify-center
, anditems-center
utility classes are used to center the sign-in form vertically and horizontally, ensuring a clean and responsive layout. Themax-w-md
class is applied to restrict the width of the sign-in form to a medium size for better presentation on different screen sizes.
Now, when you navigate to /sign-in
in your application, it will render the sign-in page with the form and authentication options.
Create a Layout for the Sign-In Page
To ensure consistency in the design and layout of the sign-in page, you can create a layout.tsx
file inside the sign-in
folder. This layout will serve as a wrapper for the sign-in page and allow you to manage common elements like headers, footers, or sidebars if needed in the future.
Here’s how to structure the layout.tsx
file:
import React from 'react'
const SignupLayout = ({children}: {children: React.ReactNode}) => {
return (
<div className='flex items-center justify-center w-screen h-screen'>{children}</div>
)
}
export default SignupLayout
Explanation:
-
Layout Component: The
SignInLayout
component wraps the entire sign-in page content (children
). This layout is useful for organizing the structure of the page and allows for consistent styling and placement of content. -
Styling: The layout applies utility classes like
min-h-screen
,bg-gray-50
,flex
,justify-center
, anditems-center
to center the content both vertically and horizontally on the page. Additionally, thew-full max-w-md
classes control the width of the container, whilep-6
,bg-white
,rounded-lg
, andshadow-lg
provide padding, background color, rounded corners, and a shadow effect for a polished look.
Step 6 - Run the Application:
To start your development server and preview your application:
- Open your terminal and navigate to your project directory.
- Run the following command:
pnpm dev
This command will launch the development server and make your application accessible at http://localhost:3000
by default. You can now test and view your application in the browser.
Once the development server is running, visit the following URL in your browser to access the sign-in page:
http://localhost:3000/signin
This will load the sign-in page of your application, where you can test the OAuth authentication flow.
Congratulations! You have successfully integrated OAuth login into your application, providing users with a seamless and secure authentication experience. 🎉
If you enjoyed this blog, please consider following and supporting me! Your encouragement motivates me to create more helpful content for you. 😊
Reference Links:
Better_Auth Docs: https://www.better-auth.com/
pnpm Docs: https://pnpm.io/
Docker Docs: https://docs.docker.com/
Prisma Docs: https://www.prisma.io/docs/getting-started
Shadcn Docs: https://ui.shadcn.com/
Next.js Docs: https://nextjs.org/
Tailwindcss Docs: https://tailwindcss.com/
Github repository: https://github.com/Daanish2003/better_auth_nextjs
Top comments (0)