Building Full stack application with NestJs, NextJs and Supabase
When i started my SaaS journey i decided to go with Nest + Next + Supabase tech stack but i was unable to find any good resource which showcased e2e integration with supabase.
After few days of playing around, i was able to implement the Nestjs + NextJs + Supabase stack. I have been using same stack for all of my side projects. I have compiled the implementation in steps below.
We will divide this into frontend and backend section.
First let’s start with the backend.
NestJs with Supabase
Create new nest project
nest new nest-supabase-boilerplate
cd nest-supabase-boilerplate
we will need following modules, install these using npm
npm i @nestjs/config @nestjs/passport @supabase/supabase-js @nestjs/jwt passport-jwt
Create a auth module in nest application
nest g module auth
create two folders under auth/ with name guards and strategies
create a auth guard named jwt.auth.guard.ts
in guards
folder, we will use this guard to protect our routes. add the following contents to the jwt.auth.guard.ts
file
src/auth/guards/jwt.auth.guard.ts
import { AuthGuard } from '@nestjs/passport'
export class JwtAuthGuard extends AuthGuard('jwt') {}
create a strategy with file name supabase.strategy.ts
with following contentssrc/auth/strategies/supabase.strategy.ts
import { Injectable } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport'
import { ExtractJwt, Strategy } from 'passport-jwt'
import { ConfigService } from '@nestjs/config'
@Injectable()
export class SupabaseStrategy extends PassportStrategy(Strategy) {
public constructor(private readonly configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
})
}
async validate(payload: any): Promise<any> {
return payload
}
authenticate(req) {
super.authenticate(req)
}
}
The src/ folder structure should look like this
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── auth
│ ├── auth.module.ts
│ ├── guards
│ │ └── jwt.auth.guard.ts
│ └── strategies
│ └── supabase.strategy.ts
└── main.ts
Now let’s include the created jwt guard and supabase strategy in auth module like so
src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PassportModule } from '@nestjs/passport'
import { JwtModule } from '@nestjs/jwt';
import { JwtAuthGuard } from './guards/jwt.guard';
import { SupabaseStrategy } from './strategies/supabase.strategy';
@Module({
imports: [
PassportModule,
ConfigModule,
JwtModule.registerAsync({
useFactory: (configService: ConfigService) => {
return {
global: true,
secret: configService.get<string>('JWT_SECRET'),
signOptions: { expiresIn: 40000 },
}
},
inject: [ConfigService],
}),
],
providers: [JwtAuthGuard, SupabaseStrategy],
exports: [JwtAuthGuard, JwtModule]
})
export class AuthModule {}
Add JWT_SECRET
to your .env
file
JWT_SECRET=YOUR_JWT_SECRET_FROM_SUPABASE
We have used ConfigModule
of nestjs to read env file. add ConfigModule
to app.module.ts
in imports with option isGlobal
as true
src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module';
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [AuthModule, ConfigModule.forRoot({ isGlobal: true })],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Now that we have our auth part setup, we can add JwtAuthGuard
to our routes to protect them. Let's try it with one of our route from app controller.
I have added new route /protected
in app.controller.ts
src/app.controller.ts
import { Controller, Get, Req, UseGuards } from '@nestjs/common';
import { AppService } from './app.service';
import { JwtAuthGuard } from './auth/guards/jwt.auth.guard';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
@Get('/protected')
@UseGuards(JwtAuthGuard)
async protected(@Req() req) {
return {
"message": "AuthGuard works 🎉",
"authenticated_user": req.user
};
}
}
Now let’s run our server and try hitting the endpoint. Replace the YOUR_SUPABSE_SESSION_ACCESS_TOKEN
with the access_token you get on the frontend in the session
object of the supabase.
curl -X GET 'http://localhost:3005/protected' \
-H 'Authorization: bearer YOUR_SUPABSE_SESSION_ACCESS_TOKEN'
here’s the response from the api
{
"message": "AuthGuard works 🎉",
"authenticated_user": {
"iss": "https://yoursupabaseid.supabase.co/auth/v1",
"sub": "e841cda5-1428-4e62-9c83-039d393e589b",
"aud": "authenticated",
"exp": 1727865444,
"iat": 1727861844,
"email": "youremail@gmail.com",
"phone": "",
"app_metadata": {
"provider": "email",
"providers": [
"email"
]
},
"user_metadata": {
"email": "youremail@gmail.com",
"email_verified": false,
"phone_verified": false,
"sub": "e841cda5-1428-4e62-9c83-039d393e589b"
},
"role": "authenticated",
"aal": "aal1",
"amr": [
{
"method": "password",
"timestamp": 1727861844
}
],
"session_id": "fafa54d0-7abf-44e0-b81c-5d45a364b496",
"is_anonymous": false
}
}
Now let’s move on to the frontend part
NextJs + Supabase
In this section we will authenticate user on frontend and pass the access_token in the protected api in Authorization header. we will keep this minimal with login functionality
Let’s start by creating new Nextjs project
npx create-next-app@latest
Install the following packages
npm i @supabase/ssr axios
Now create a lib/supabase
in root dir for adding supabase utils functions to initiate supabase clients. the structure will look something like this
.next/
app/
lib/
└── supabase/
├── client.ts
└── server.ts
.env
Add below code to client.ts
to initialize frontend client.lib/supabase/client.ts
import { createBrowserClient } from "@supabase/ssr";
export const getSupabaseFrontendClient = () => {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
)
}
Add below code to server.ts
lib/supabase/server.ts
"use server";
import { createServerClient, type CookieOptions } from '@supabase/ssr'
import { cookies } from 'next/headers'
export default async function createSupabaseServerClient() {
const cookieStore = cookies()
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
set(name: string, value: string, options: CookieOptions) {
try {
cookieStore.set({ name, value, ...options })
} catch (error) {}
},
remove(name: string, options: CookieOptions) {
try {
cookieStore.set({ name, value: '', ...options })
} catch (error) {}
},
},
}
)
}
Now let’s create nextjs server actions for login and registering user.
Create new file under app/auth/actions
with name index.ts
app/
├── auth/
│ └── actions/
│ └── index.ts
Write your login and register logic here. you can customise this as per your need. These are server actions. This is only part where you will need to use server actions.app/auth/actions/index.ts
"use server"
import createSupabaseServerClient from "@/lib/supabase/server";
export async function singInWithEmailAndPassword(data: {
email: string;
password: string;
}) {
const supabase = await createSupabaseServerClient();
const result = await supabase.auth.signInWithPassword({ email: data.email, password: data.password});
return result;
}
export async function singUpWithEmailAndPassword(data: {
email: string;
password: string;
}) {
const supabase = await createSupabaseServerClient();
const result = await supabase.auth.signUp({ email: data.email, password: data.password, options: {
emailRedirectTo: `${process.env.NEXT_PUBLIC_FRONTEND_URL}/login`
}});
return result;
}
Now let’s create simple /login
route in app
dirapp/login/page.tsx
'use client';
import { useState } from 'react';
import { singInWithEmailAndPassword } from '../auth/actions';
import { useRouter } from 'next/navigation';
export default function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const { data, error } = await singInWithEmailAndPassword({ email, password });
if (error) {
console.error('Error logging in:', error);
} else {
console.log('Logged in successfully:', data);
router.push('/dashboard');
}
console.log('Login attempt with:', { email, password });
};
return (
<div>
<h1>Login</h1>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email</label>
<input
type="email"
id="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
type="password"
id="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<button type="submit">Log In</button>
</form>
</div>
);
}
Now, once the user is logged in we will redirect user to dashboard and call /protected
route we created on backend. for this we will make use of axios with some modifications. for every request we send to backend, we will need to inject Authorization
header. for this we will create a axios wrapper / hook which we will use to call api everytime.
Let’s create this, create a axios.ts
file under lib/
dir and useAxiosAuth.ts
under lib/hooks/
.next/
app/
lib/
├── axios.ts
├── hooks
│ └── useAxiosAuth.ts
└── supabase
├── client.ts
└── server.ts
Add your backend api base url in axios.create
lib/axios.ts
import axios from "axios";
const BASE_URL = process.env.NEXT_PUBLIC_BACKEND_API_URL;
export default axios.create({
baseURL: BASE_URL,
headers: { "Content-Type": "application/json" },
})
export const axiosAuth = axios.create({
baseURL: BASE_URL,
headers: { "Content-Type": "application/json" },
})
lib/hooks/useAxiosAuth.ts
'use client';
import { useEffect } from "react";
import { axiosAuth } from "../axios";
import { getSupabaseFrontendClient } from "../supabase/client";
const useAxiosAuth = () => {
const supabase = getSupabaseFrontendClient();
useEffect(() => {
const requestIntercept = axiosAuth.interceptors.request.use(async (config) => {
const { data: session} = await supabase.auth.getSession();
let accessToken = session?.session?.access_token;
if (!config.headers['Authorization']) {
config.headers['Authorization'] = `bearer ${accessToken}`
}
return config;
},
(error) => Promise.reject(error)
);
return () => {
axiosAuth.interceptors.request.eject(requestIntercept);
}
}, [])
return axiosAuth;
}
export default useAxiosAuth;
Now, let’s create a /dashboard
route which will have session check. and we will request /protected
route we created on the backend with the supabase access_token
app/dashboard/page.tsx
"use client"
import useAxiosAuth from "@/lib/hooks/useAxiosAuth";
import { getSupabaseFrontendClient } from "@/lib/supabase/client";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react"
export default function DashboardPage() {
const router = useRouter();
const [user, setUser] = useState<any>(null);
const supabase = getSupabaseFrontendClient();
const axiosAuth = useAxiosAuth();
const getProtectedData = async () => {
const response = await axiosAuth.get('/protected');
console.log('Protected data:', response.data);
}
useEffect(() => {
const checkSession = async () => {
const { data } = await supabase.auth.getSession();
console.log('Session:', data);
if (!data.session) {
router.push('/login');
} else {
setUser(data.session.user);
getProtectedData();
}
}
checkSession();
}, []);
const logout = async () => {
await supabase.auth.signOut();
router.push('/login');
}
return <div>
<h1>Dashboard</h1>
<p>Welcome to the dashboard {user?.email}</p>
<button onClick={logout}>Logout</button>
</div>
}
This is the response we get in the console after requesting the protected route
Protected data: {message: 'AuthGuard works 🎉', authenticated_user: {…}}
That’s it! You can use supabase as admin to interact with database on backend or use any orm with pg connection.
Links for both next and nest boilerplate
nestjs — https://github.com/shobhit99/nestjs-supabase-boilerplate
nextjs — https://github.com/shobhit99/nextjs-supabase-boilerplate
I share cool stuff and learnings on twitter, you can follow me here — https://x.com/nullbytes00
Thanks!!