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.
Create a Supabase Project
- Sign Up or Log In: Visit supabase.com and create an account or log in if you already have one.
- Create a New Project: Once logged in, click "New Project" and fill in the necessary details such as organisation, project name, and password. Choose a database region that is closest to your users.
💡 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.
- Click on Create new project and wait for the process to finish.
Get your API keys
Let’s get our Project URL and API keys required to connect our application to our Supabase project.
- Navigate to the Supabase Dashboard and click on the Project you just created.
- In the left menu, at the bottom, click Project Settings, and then click Data API
- Find your Project URL and Public Anon Key here:
- Create a
.env
file in the root of your Next.js application and paste your keys like this
NEXT_PUBLIC_SUPABASE_URL=<your-project-url>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<your-anon-key>
Creating Client and Server Supabase Client
Supabase uses two different clients to connect to the Supabase project
- Server Client: This will be used by Server Components, Server Actions, and anything else that runs on our NextJS Server.
Browser Client: This will be used by Client Components, Hooks, Stores, and anything else that runs on our Browser UI.
Install the supabase ssr package
pnpm add @supabase/ssr @supabase/supabase-js
- Create a folder called
utils/supabase
. We will use this folder to create Supabase Browser Client, Supabase Server Client, and Supabase Middleware to handle routing for us.
Supabase Browser Client /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!,
);
Supabase Server Client /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);
}
},
},
},
);
};
- Before creating the middleware, let’s define what routes are in the client, and which routes are allowed only when the user is logged in. Add the following lines to the end of the constants file
/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
- Let’s create a dummy dashboard route that will be protected by our middleware, defined in the
PROTECTED_ROUTES
above.
Dashboard Page /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
Supabase Middleware /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.
Next.js Middleware /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.
Adding Supabase Callbacks and Server Actions to handle 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.
- Callback Route: This route helps Supabase to verify a temporary token to let users reset their password, even when they are not logged in. Supabase sends a temporary token to let the user authenticate to our application temporarily just to change the password.
- Confirm Route: This route helps Supabase identify new users, and allows the signup to our application. Then this route redirects the user to the specified page where we want the user to visit.
Callback /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}`);
}
Confirm /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.
Supabase Actions /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:
Signup /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!");
}
}
Login /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);
}
}
Forgot Password /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");
}
}
Reset Password /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.
💡Tips
- In case of any errors, look for any console logs in the browser console as well as the server terminal. Also check Supabase logs in your Supabase project.
- For production you should always configure your own custom SMTP server to work with Supabase.
- Always set a low time limit for the auth token generated using the
/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?
Top comments (0)