Building Full stack application with NestJs, NextJs and Supabase

Shobhit Bhosure
7 min readJust now

--

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.tsfile

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 contents
src/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 dir
app/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!!

--

--