October 29, 2024
O. Wolfson
Next.js 15 brings new enhancements, including Promise-based searchParams, improved server performance, and a seamless integration with React 19. In this guide, we’ll leverage these features to build a secure CRUD application with Supabase authentication.
First, set up your environment by installing the required Supabase packages:
bashnpm install @supabase/supabase-js @supabase/ssr
Then, in your project root, create a .env.local file to store your Supabase environment variables:
plaintextNEXT_PUBLIC_SUPABASE_URL=<your_supabase_project_url> NEXT_PUBLIC_SUPABASE_ANON_KEY=<your_supabase_anon_key>
These values are available in your Supabase dashboard.
We’ll create two client utilities for Supabase: one for client components and one for server components.
utils/supabase/client.ts)typescriptimport { createBrowserClient } from "@supabase/ssr";
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL ?? "",
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? ""
);
}
utils/supabase/server.ts)The server client requires handling cookies securely. In Next.js 15, the cookies API is asynchronous:
typescriptimport { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL ?? "",
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? "",
{
cookies: {
getAll: () => cookieStore.getAll(),
setAll: (cookiesToSet) => {
for (const { name, value, options } of cookiesToSet) {
cookieStore.set(name, value, options);
}
},
},
}
);
}
Next, we’ll add middleware to refresh tokens and redirect users when necessary.
Create a middleware.ts file in your project root:
typescriptimport 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: [
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};
Create a helper for this in utils/supabase/middleware.ts:
typescriptimport { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
export async function updateSession(request: NextRequest) {
let supabaseResponse = NextResponse.next({
request,
});
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL ?? "",
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ?? "",
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
for (const { name, value } of cookiesToSet) {
request.cookies.set(name, value);
}
supabaseResponse = NextResponse.next({
request,
});
for (const { name, value, options } of cookiesToSet) {
supabaseResponse.cookies.set(name, value, options);
}
},
},
}
);
const {
data: { user },
} = await supabase.auth.getUser();
(!user && !request...()) {
url = request..();
url. = ;
.(url);
}
supabaseResponse;
}
This middleware handles user sessions by ensuring that expired tokens are refreshed.
Now, we’ll build our login and signup pages, utilizing server actions for secure, server-side authentication.
Login Page (app/login/page.tsx):
typescriptimport { login, signup } from "./actions";
export default function LoginPage() {
return (
<form>
<label htmlFor="email">Email:</label>
<input id="email" name="email" type="email" required />
<label htmlFor="password">Password:</label>
<input id="password" name="password" type="password" required />
<button formAction={login}>Log in</button>
<button formAction={signup}>Sign up</button>
</form>
);
}
Server Actions (app/login/actions.ts):
typescript"use server";
import { redirect } from "next/navigation";
import { createClient } from "@/utils/supabase/server";
export async function login(formData: FormData) {
const supabase = await createClient();
const { error } = await supabase.auth.signInWithPassword({
email: formData.get("email") as string,
password: formData.get("password") as string,
});
if (error) redirect("/error");
redirect("/");
}
export async function signup(formData: FormData) {
const supabase = await createClient();
const { error } = await supabase.auth.signUp({
email: formData.get("email") as string,
password: formData.get("password") as ,
});
(error) ();
();
}
With authentication in place, let’s implement our basic CRUD functionality. Here’s an example of a page that reads and displays data:
Data Page (app/data/page.tsx):
typescriptimport { createClient } from "@/utils/supabase/server";
export default async function DataPage() {
const supabase = await createClient();
const { data, error } = await supabase.from("my_table").select("*");
if (error) return <p>Error loading data</p>;
return (
<div>
{data?.map((item) => (
<p key={item.id}>{item.name}</p>
))}
</div>
);
}
To create, update, or delete records, similar functions can be created as Server Actions that call Supabase methods like insert, update, and delete.
If email confirmation is enabled, users will need to confirm their account. Modify the email confirmation URL in Supabase to direct users to auth/confirm with the token.
Auth Confirmation Route (app/auth/confirm/route.ts):
typescriptimport { type EmailOtpType } from "@supabase/supabase-js";
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
export async function GET({ url }: { url: URL }) {
const searchParams = new URL(url).searchParams;
const token_hash = searchParams.get("token_hash");
const type = searchParams.get("type") as EmailOtpType;
if (token_hash && type) {
const supabase = await createClient();
const { error } = await supabase.auth.verifyOtp({ type, token_hash });
if (!error) redirect("/");
}
redirect("/error");
}
For private routes, use supabase.auth.getUser() to check the session.
We’ve covered the full process of setting up a secure Next.js 15 CRUD application with Supabase authentication. This includes configuring clients, setting up middleware, creating login pages, implementing CRUD functions, and handling private routes and email confirmation. This structure provides a solid foundation for secure, authenticated CRUD applications.