Prepare for the PgBouncer and IPv4 deprecations on 26th January 2024

Home

Setting up Server-Side Auth for Next.js

Next.js comes in two flavors: the App Router and the Pages Router. You can set up Server-Side Auth with either strategy. You can even use both in the same application.

1

Install Supabase packages

Install the @supabase/supabase-js package and the helper @supabase/ssr package.


_10
npm install @supabase/supabase-js @supabase/ssr

2

Set up environment variables

Create a .env.local file in your project root directory.

Fill in your NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY:

Project URL
Anon key
.env.local

_10
NEXT_PUBLIC_SUPABASE_URL=<your_supabase_project_url>
_10
NEXT_PUBLIC_SUPABASE_ANON_KEY=<your_supabase_anon_key>

3

Write utility functions to create Supabase clients

To access Supabase from your Next.js app, you need 2 types of Supabase clients:

  1. Client Component client - To access Supabase from Client Components, which run in the browser.
  2. Server Component client - To access Supabase from Server Components, Server Actions, and Route Handlers, which run only on the server.

Create a utils/supabase folder with a file for each type of client. Then copy the utility functions for each client type.

utils/supabase/client.ts
utils/supabase/server.ts

_10
import { createBrowserClient } from '@supabase/ssr'
_10
_10
export function createClient() {
_10
return createBrowserClient(
_10
process.env.NEXT_PUBLIC_SUPABASE_URL!,
_10
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
_10
)
_10
}

4

Hook up middleware

Create a middleware.ts file at the root of your project.

Since Server Components can't write cookies, you need middleware to refresh expired Auth tokens and store them.

The middleware is responsible for:

  1. Refreshing the Auth token (by calling supabase.auth.getUser).
  2. Passing the refreshed Auth token to Server Components, so they don't attempt to refresh the same token themselves. This is accomplished with request.cookies.set.
  3. Passing the refreshed Auth token to the browser, so it replaces the old token. This is accomplished with response.cookies.set.

Copy the middleware code for your app.

Add a matcher so the middleware doesn't run on routes that don't access Supabase.

middleware.ts

_73
import { createServerClient, type CookieOptions } from '@supabase/ssr'
_73
import { NextResponse, type NextRequest } from 'next/server'
_73
_73
export async function middleware(request: NextRequest) {
_73
let response = NextResponse.next({
_73
request: {
_73
headers: request.headers,
_73
},
_73
})
_73
_73
const supabase = createServerClient(
_73
process.env.NEXT_PUBLIC_SUPABASE_URL!,
_73
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
_73
{
_73
cookies: {
_73
get(name: string) {
_73
return request.cookies.get(name)?.value
_73
},
_73
set(name: string, value: string, options: CookieOptions) {
_73
request.cookies.set({
_73
name,
_73
value,
_73
...options,
_73
})
_73
response = NextResponse.next({
_73
request: {
_73
headers: request.headers,
_73
},
_73
})
_73
response.cookies.set({
_73
name,
_73
value,
_73
...options,
_73
})
_73
},
_73
remove(name: string, options: CookieOptions) {
_73
request.cookies.set({
_73
name,
_73
value: '',
_73
...options,
_73
})
_73
response = NextResponse.next({
_73
request: {
_73
headers: request.headers,
_73
},
_73
})
_73
response.cookies.set({
_73
name,
_73
value: '',
_73
...options,
_73
})
_73
},
_73
},
_73
}
_73
)
_73
_73
await supabase.auth.getUser()
_73
_73
return response
_73
}
_73
_73
export const config = {
_73
matcher: [
_73
/*
_73
* Match all request paths except for the ones starting with:
_73
* - _next/static (static files)
_73
* - _next/image (image optimization files)
_73
* - favicon.ico (favicon file)
_73
* Feel free to modify this pattern to include more paths.
_73
*/
_73
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
_73
],
_73
}

5

Create a login page

Create a login page for your app. Use a Server Action to call the Supabase signup function.

Since Supabase is being called from an Action, use the client defined in @/utils/supabase/server.ts.

app/login/page.tsx
app/login/actions.ts
app/error/page.tsx

_14
import { login, signup } from './actions'
_14
_14
export default function LoginPage() {
_14
return (
_14
<form>
_14
<label htmlFor="email">Email:</label>
_14
<input id="email" name="email" type="email" required />
_14
<label htmlFor="password">Password:</label>
_14
<input id="password" name="password" type="password" required />
_14
<button formAction={login}>Log in</button>
_14
<button formAction={signup}>Sign up</button>
_14
</form>
_14
)
_14
}

6

Change the Auth confirmation path

If you have email confirmation turned on (the default), a new user will receive an email confirmation after signing up.

Change the email template to support a server-side authentication flow.

Go to the Auth templates page in your dashboard. In each email template, change {{ .ConfirmationURL }} to {{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=signup.

7

Create a route handler for Auth confirmation

Create a Route Handler for auth/confirm. When a user clicks their confirmation email link, exchange their secure code for an Auth token.

Since this is a Router Handler, use the Supabase client from @/utils/supabase/server.ts.

app/auth/confirm/route.ts

_36
import { type EmailOtpType } from '@supabase/supabase-js'
_36
import { cookies } from 'next/headers'
_36
import { type NextRequest, NextResponse } from 'next/server'
_36
_36
import { createClient } from '@/utils/supabase/server'
_36
_36
export async function GET(request: NextRequest) {
_36
const cookieStore = cookies()
_36
_36
const { searchParams } = new URL(request.url)
_36
const token_hash = searchParams.get('token_hash')
_36
const type = searchParams.get('type') as EmailOtpType | null
_36
const next = searchParams.get('next') ?? '/'
_36
_36
const redirectTo = request.nextUrl.clone()
_36
redirectTo.pathname = next
_36
redirectTo.searchParams.delete('token_hash')
_36
redirectTo.searchParams.delete('type')
_36
_36
if (token_hash && type) {
_36
const supabase = createClient(cookieStore)
_36
_36
const { error } = await supabase.auth.verifyOtp({
_36
type,
_36
token_hash,
_36
})
_36
if (!error) {
_36
redirectTo.searchParams.delete('next')
_36
return NextResponse.redirect(redirectTo)
_36
}
_36
}
_36
_36
// return the user to an error page with some instructions
_36
redirectTo.pathname = '/error'
_36
return NextResponse.redirect(redirectTo)
_36
}

8

Access user info from Server Component

Server Components can read cookies, so you can get the Auth status and user info.

Since you're calling Supabase from a Server Component, use the client created in @/utils/supabase/server.ts.

Create a private page that users can only access if they're logged in. The page displays their email.

app/private/page.tsx

_16
import { cookies } from 'next/headers'
_16
import { redirect } from 'next/navigation'
_16
_16
import { createClient } from '@/utils/supabase/server'
_16
_16
export default async function PrivatePage() {
_16
const cookieStore = cookies()
_16
const supabase = createClient(cookieStore)
_16
_16
const { data, error } = await supabase.auth.getUser()
_16
if (error || !data?.user) {
_16
redirect('/')
_16
}
_16
_16
return <p>Hello {data.user.email}</p>
_16
}

Congratulations

You're done! To recap, you've successfully:

  • Called Supabase from a Server Action.
  • Called Supabase from a Server Component.
  • Set up a Supabase client utility to call Supabase from a Client Component. You can use this if you need to call Supabase from a Client Component, for example to set up a realtime subscription.
  • Set up middleware to automatically refresh the Supabase Auth session.

You can now use any Supabase features from your client or server code!

Troubleshooting