2024-10-31 Web Development
Building a Full-Stack CRUD Application with Next.js 15 and Supabase Authentication By 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.
1. Initial Setup and Installation
First, set up your environment by installing the required Supabase packages:
bash Copy
npm install @supabase/supabase-js @supabase/ssr
Then, in your project root, create a .env.local
file to store your Supabase environment variables:
plaintext Copy
NEXT_PUBLIC_SUPABASE_URL=<your_supabase_project_url>
NEXT_PUBLIC_SUPABASE_ANON_KEY=<your_supabase_anon_key>
These values are available in your Supabase dashboard.
2. Configuring Supabase Client Utilities
We’ll create two client utilities for Supabase: one for client components and one for server components.
Client Component Client (utils/supabase/client.ts
)
typescript Copy
import { createBrowserClient } from "@supabase/ssr" ;
export function createClient ( ) {
return createBrowserClient (
process.env .NEXT_PUBLIC_SUPABASE_URL ?? "" ,
process.env .NEXT_PUBLIC_SUPABASE_ANON_KEY ?? ""
);
}
Server Component Client (utils/supabase/server.ts
)
The server client requires handling cookies securely. In Next.js 15, the cookies API is asynchronous:
typescript Copy
import { 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);
}
},
},
}
);
}
3. Setting Up Middleware for Session Refresh and Authentication
Next, we’ll add middleware to refresh tokens and redirect users when necessary.
Create a middleware.ts
file in your project root:
typescript Copy
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 : [
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)" ,
],
};
Create a helper for this in utils/supabase/middleware.ts
:
typescript Copy
import { 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 ();
if (!user && !request.nextUrl .pathname .startsWith ("/login" )) {
const url = request.nextUrl .clone ();
url.pathname = "/login" ;
return NextResponse .redirect (url);
}
return supabaseResponse;
}
This middleware handles user sessions by ensuring that expired tokens are refreshed.
4. Creating Login and Signup Pages with Server Actions
Now, we’ll build our login and signup pages, utilizing server actions for secure, server-side authentication.
Login Page (app/login/page.tsx
):
typescript Copy
import { 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 Copy
"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 string ,
});
if (error) redirect ("/error" );
redirect ("/" );
}
5. Implementing CRUD Operations
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
):
typescript Copy
import { 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
.
6. Adding Auth Confirmation and Private Routes
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
):
typescript Copy
import { 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.
Conclusion
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.
Let me know if you'd like to expand on any section, add more examples, or clarify specific points!