Secure Your Next.js App: Email & Google Authentication with Supabase, PostgreSQL RLS, and Triggers - Part 2
Published on March 15, 2025
This article might not be well formatted due to markdown render issues.
Supabase is a powerful backend-as-a-service that provides a PostgreSQL database, authentication, and storage, making it an excellent alternative to Firebase. Here's how to create a Supabase project and set up the necessary environment variables for your Next.js application.
🔗 Check out the full code for this series here.
💡 Tip: Save your Database Password somewhere safe. You’ll get this one-time, and we will need it. You can always reset the Database Password in the Project Settings if you forget it.
Let’s get our Project URL and API keys required to connect our application to our Supabase project.
.env
file in the root of your Next.js application and paste your keys like thisNEXT_PUBLIC_SUPABASE_URL=<your-project-url>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<your-anon-key>
Supabase uses two different clients to connect to the Supabase project
pnpm add @supabase/ssr @supabase/supabase-js
utils/supabase
. We will use this folder to create Supabase Browser Client, Supabase Server Client, and Supabase Middleware to handle routing for us./utils/supabase/client.ts
import { createBrowserClient } from "@supabase/ssr";
export const createClient = () =>
createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
/utils/supabase/server.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export const createClient = async () => {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) => {
cookieStore.set(name, value, options);
});
} catch (error) {
// The `set` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
console.error(error);
}
},
},
},
);
};
/lib/constants
export const CLIENT_ROUTES = {
HOME: "/",
// app
DASHBOARD: "/dashboard",
RESET_PASSWORD: "/reset-password",
// auth
LOGIN: "/login",
SIGNUP: "/signup",
FORGOT_PASSWORD: "/forgot-password",
};
export const PROTECTED_ROUTES = [
CLIENT_ROUTES.DASHBOARD,
CLIENT_ROUTES.RESET_PASSWORD,
];
export enum EServerResponseCode {
// good practice
SUCCESS,
FAILURE,
}
💡 Tip: After adding these routes, you can use them for href linking. It is considered a better practice over using plain strings everywhere.
Also, let’s add lodash to make our lives easier while working with JS functions.
pnpm add lodash-es
pnpm add -D @types/lodash-es
PROTECTED_ROUTES
above./app/dashboard/page.tsx
"use client";
import { Button } from "@/components/ui/button";
import { signOutAction } from "@/actions/supabase";
import { useRouter } from "next/navigation";
import { CLIENT_ROUTES } from "@/lib/constants";
export default function DashboardPage() {
const router = useRouter();
async function onLogout() {
try {
await signOutAction();
router.push(CLIENT_ROUTES.LOGIN);
} catch (error) {
alert("Some error occured");
}
}
return (
<div className="mt-32 text-center">
This is a protected dashboard page. You can only view this page when
you're logged in.
<form action={onLogout} className="mt-4">
<Button>Logout</Button>
</form>
</div>
);
}
Now let’s create our Supabase Middleware to handle auth routing
/utils/supabase/middleware
import { createServerClient } from "@supabase/ssr";
import { includes, isEmpty } from "lodash-es";
import { type NextRequest, NextResponse } from "next/server";
import { CLIENT_ROUTES, PROTECTED_ROUTES } from "@/lib/constants";
export const updateSession = async (request: NextRequest) => {
// This `try/catch` block is only here for the interactive tutorial.
// Feel free to remove once you have Supabase connected.
const headers = new Headers(request.headers);
headers.set("x-current-path", request.nextUrl.pathname);
try {
// Create an unmodified response
let response = NextResponse.next({
request: {
headers: request.headers,
},
});
// supabase defaults
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) =>
request.cookies.set(name, value),
);
response = NextResponse.next({
request,
});
cookiesToSet.forEach(({ name, value, options }) =>
response.cookies.set(name, value, options),
);
},
},
},
);
// This will refresh session if expired - required for Server Components
// https://supabase.com/docs/guides/auth/server-side/nextjs
const {
data: { user },
} = await supabase.auth.getUser();
if (
isEmpty(user) &&
includes(PROTECTED_ROUTES, request.nextUrl.pathname)
) {
// no user, potentially respond by redirecting the user to the login page except for the above pages
const url = request.nextUrl.clone();
url.pathname = CLIENT_ROUTES.LOGIN;
return NextResponse.redirect(url, { headers });
}
if (
!isEmpty(user) &&
(request.nextUrl.pathname === CLIENT_ROUTES.LOGIN ||
request.nextUrl.pathname === CLIENT_ROUTES.SIGNUP ||
request.nextUrl.pathname === CLIENT_ROUTES.FORGOT_PASSWORD)
) {
// if the user is logged in and the url is login, signup, or forgot-password we redirect the user to dashboard.
const url = request.nextUrl.clone();
url.pathname = CLIENT_ROUTES.DASHBOARD;
return NextResponse.redirect(url, { headers });
}
return response;
} catch (error) {
// If you are here, a Supabase client could not be created!
// This is likely because you have not set up environment variables.
// Check out steps 1-4 again.
console.error(error);
return NextResponse.next({
request: {
headers,
},
});
}
};
Now let’s create a Next.js Middleware in the root folder of our application and hook the Supabase middleware we just created. Otherwise the Supabase middleware is useless.
/middleware.ts
import { type NextRequest } from "next/server";
import { updateSession } from "@/utils/supabase/middleware";
export async function middleware(request: NextRequest) {
return await updateSession(request);
}
export const config = {
matcher: [
/*
* Match all request paths except:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - images - .svg, .png, .jpg, .jpeg, .gif, .webp
* Feel free to modify this pattern to include more paths.
*/
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};
💡 Tip: I’ve added the Route handling logic to the Supabase middleware for simplicity. It should ideally be handled with a separate middleware.
Now we are ready to create Server Actions (APIs) that will help our App to login and manage authentication.
Before adding Server Actions for the users to authenticate to our application, we need to add some necessary callback URLs for Supabase to work.
/app/auth/callback/route.ts
import { isEmpty } from "lodash-es";
import { NextResponse } from "next/server";
import { CLIENT_ROUTES } from "@/lib/constants";
import { createClient } from "@/utils/supabase/server";
export async function GET(request: Request) {
// The `/auth/callback` route is required for the server-side auth flow implemented
// by the SSR package. It exchanges an auth code for the user's session.
// https://supabase.com/docs/guides/auth/server-side/nextjs
const requestUrl = new URL(request.url);
const code = requestUrl.searchParams.get("code");
const origin = requestUrl.origin;
const redirectTo = requestUrl.searchParams.get("redirect_to")?.toString();
if (code) {
const supabase = await createClient();
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (!isEmpty(error)) {
return NextResponse.redirect(
`${origin}${CLIENT_ROUTES.LOGIN}?failed=true`,
);
}
}
if (redirectTo) {
return NextResponse.redirect(`${origin}${redirectTo}`);
}
// URL to redirect to after sign up process completes
return NextResponse.redirect(`${origin}${CLIENT_ROUTES.DASHBOARD}`);
}
/app/auth/confirm/route.ts
import { type EmailOtpType } from "@supabase/supabase-js";
import { redirect } from "next/navigation";
import { type NextRequest } from "next/server";
import { CLIENT_ROUTES } from "@/lib/constants";
import { createClient } from "@/utils/supabase/server";
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const token_hash = searchParams.get("token_hash");
const type = searchParams.get("type") as EmailOtpType | null;
const next = searchParams.get("next") ?? CLIENT_ROUTES.DASHBOARD;
if (token_hash && type) {
const supabase = await createClient();
const { error } = await supabase.auth.verifyOtp({
type,
token_hash,
});
if (!error) {
// redirect user to specified redirect URL or root of app
redirect(next);
}
}
// redirect the user to an error page with some instructions
redirect("/error");
}
Now let’s add the Server Actions to handle the authentication from the client.
/actions/supabase.ts
"use server";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { CLIENT_ROUTES, EServerResponseCode } from "@/lib/constants";
import {
ForgotPasswordFormSchema,
ResetPasswordFormSchema,
SignupFormSchema,
type TForgotPasswordFormSchema,
type TLoginFormSchema,
type TResetPasswordFormSchema,
type TSignupFormSchema,
} from "@/lib/constants";
import { createClient } from "@/utils/supabase/server";
export const signupAction = async (formData: TSignupFormSchema) => {
const { email, password } = formData;
const supabase = await createClient();
const origin = (await headers()).get("origin");
const validation = SignupFormSchema.safeParse(formData);
if (!validation.success) {
return {
code: EServerResponseCode.FAILURE,
error: validation.error,
message: "Signup failed",
};
}
const { error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${origin}/auth/callback`,
},
});
if (error) {
console.error(error.code + " " + error.message);
return {
code: EServerResponseCode.FAILURE,
error,
message: "Something went wrong! Please try again",
};
} else {
return {
code: EServerResponseCode.SUCCESS,
message:
"Signup successful! Please check your mail to confirm your account",
};
}
};
export const loginAction = async (formData: TLoginFormSchema) => {
const { email, password } = formData;
const supabase = await createClient();
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
console.error(error.code + " " + error.message);
return {
code: EServerResponseCode.FAILURE,
error,
message: "Invalid credentials",
};
} else {
redirect(CLIENT_ROUTES.DASHBOARD);
}
};
export const forgotPasswordAction = async (
formData: TForgotPasswordFormSchema,
) => {
const { email } = formData;
const supabase = await createClient();
const origin = (await headers()).get("origin");
const validation = ForgotPasswordFormSchema.safeParse(formData);
if (!validation.success) {
return {
code: EServerResponseCode.FAILURE,
error: validation.error,
message: "Password reset failed! Please try again",
};
}
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${origin}/auth/callback?redirect_to=/${CLIENT_ROUTES.RESET_PASSWORD}`,
});
if (error) {
console.error(error.code + " " + error.message);
return {
code: EServerResponseCode.FAILURE,
error,
message: "Something went wrong! Please try again",
};
} else {
return {
code: EServerResponseCode.SUCCESS,
message: "Success! Please check your mail to reset your password",
};
}
};
export const resetPasswordAction = async (
formData: TResetPasswordFormSchema,
) => {
const supabase = await createClient();
const { password } = formData;
const validation = ResetPasswordFormSchema.safeParse(formData);
if (!validation.success) {
return {
code: EServerResponseCode.FAILURE,
error: validation.error,
message: "Password reset failed! Please try again",
};
}
const { error } = await supabase.auth.updateUser({
password: password,
});
if (error) {
return {
code: EServerResponseCode.FAILURE,
error,
message: "Something went wrong! Please try again",
};
} else {
return {
code: EServerResponseCode.SUCCESS,
message: "Password changed successfully",
};
}
};
export const signOutAction = async () => {
try {
const supabase = await createClient();
await supabase.auth.signOut();
return {
code: EServerResponseCode.SUCCESS,
message: "User logged out successfully!",
};
} catch (error) {
console.error(error);
return {
code: EServerResponseCode.FAILURE,
message: "Failed to logout! Please try again",
};
}
};
Now let’s edit our Form Handlers to call these functions. Edit the following pages:
/app/signup/page.tsx
async function onSubmit(values: TSignupFormSchema) {
try {
const response = await signupAction(values);
if (isEmpty(response) || response.code !== EServerResponseCode.SUCCESS) {
alert("Failed to signup!");
} else {
form.reset();
alert(response.message);
}
} catch (error) {
console.error("Signup failed:", error);
alert("Failed to signup!");
}
}
/app/login/page.tsx
async function onSubmit(values: TLoginFormSchema) {
try {
const response = await loginAction(values);
if (!isEmpty(response) && response.code === EServerResponseCode.FAILURE) {
alert(response.message);
}
} catch (error) {
console.error("Login failed:", error);
}
}
/app/forgot-password/page.tsx
async function onSubmit(values: TForgotPasswordFormSchema) {
try {
const response = await forgotPasswordAction(values);
if (isEmpty(response) || response.code !== EServerResponseCode.SUCCESS) {
alert("Failed to send verification link. Please try again");
} else {
alert("Verification link send. Please check your email");
form.reset();
}
} catch (error) {
console.error("Forgot password failed:", error);
alert("Failed to send verification link. Please try again");
}
}
/app/reset-password/page.tsx
// make sure you import the router from next/navigation
import { useRouter } from "next/navigation";
const router = useRouter();
async function onSubmit(values: TResetPasswordFormSchema) {
try {
const response = await resetPasswordAction(values);
if (
isEmpty(response) ||
response.code !== EServerResponseCode.SUCCESS
) {
alert(response.message);
console.log(response.error);
} else {
form.reset();
alert(response.message);
router.push(CLIENT_ROUTES.DASHBOARD);
}
} catch (error) {
console.error("Password reset error:", error);
alert("Password reset failed! Please try again");
}
}
Our application is ready with Email authentication. The users will be able to sign up, login, and reset their password via these routes. Test these routes and actions carefully.
/forgot-password
route.🔗 Check out the full code for this series here.
If you found this article useful, like, comment, and share, or just buy me a coffee?