Home

Supabase Auth with the Next.js App Router

The Next.js Auth Helpers package configures Supabase Auth to store the user's session in a cookie, rather than localStorage. This makes it available across the client and server of the App Router - Client Components, Server Components, Server Actions, Route Handlers and Middleware. The session is automatically sent along with any requests to Supabase.

Install Next.js Auth helpers library#

Terminal

_10
npm install @supabase/auth-helpers-nextjs @supabase/supabase-js

Declare environment variables#

Retrieve your project's URL and anon key from your API settings, and create a .env.local file with the following environment variables:

.env.local

_10
NEXT_PUBLIC_SUPABASE_URL=your-supabase-url
_10
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-supabase-anon-key

Managing session with middleware#

When using the Supabase client on the server, you must perform extra steps to ensure the user's auth session remains active. Since the user's session is tracked in a cookie, we need to read this cookie and update it if necessary.

Next.js Server Components allow you to read a cookie but not write back to it. Middleware on the other hand allow you to both read and write to cookies.

Next.js Middleware runs immediately before each route is rendered. To avoid unnecessary execution, we include a matcher config to decide when the middleware should run. You can read more on matching paths in the Next.js documentation. We'll use Middleware to refresh the user's session before loading Server Component routes.

Create a new middleware.js file in the root of your project and populate with the following:

middleware.js

_28
import { createMiddlewareClient } from '@supabase/auth-helpers-nextjs'
_28
import { NextResponse } from 'next/server'
_28
_28
export async function middleware(req) {
_28
const res = NextResponse.next()
_28
_28
// Create a Supabase client configured to use cookies
_28
const supabase = createMiddlewareClient({ req, res })
_28
_28
// Refresh session if expired - required for Server Components
_28
await supabase.auth.getSession()
_28
_28
return res
_28
}
_28
_28
// Ensure the middleware is only called for relevant paths.
_28
export const config = {
_28
matcher: [
_28
/*
_28
* Match all request paths except for the ones starting with:
_28
* - _next/static (static files)
_28
* - _next/image (image optimization files)
_28
* - favicon.ico (favicon file)
_28
* Feel free to modify this pattern to include more paths.
_28
*/
_28
'/((?!_next/static|_next/image|favicon.ico).*)',
_28
],
_28
}

Managing sign-in with Code Exchange#

The Next.js Auth Helpers are configured to use the server-side auth flow to sign users into your application. This requires you to setup a Code Exchange route, to exchange an auth code for the user's session, which is set as a cookie for future requests made to Supabase.

To make this work with Next.js, we create a callback Route Handler that performs this exchange:

Create a new file at app/auth/callback/route.js and populate with the following:

app/auth/callback/route.js

_17
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
_17
import { cookies } from 'next/headers'
_17
import { NextResponse } from 'next/server'
_17
_17
export async function GET(request) {
_17
const requestUrl = new URL(request.url)
_17
const code = requestUrl.searchParams.get('code')
_17
_17
if (code) {
_17
const cookieStore = cookies()
_17
const supabase = createRouteHandlerClient({ cookies: () => cookieStore })
_17
await supabase.auth.exchangeCodeForSession(code)
_17
}
_17
_17
// URL to redirect to after sign in process completes
_17
return NextResponse.redirect(requestUrl.origin)
_17
}

Authentication#

Authentication can be initiated client or server-side. All of the supabase-js authentication strategies are supported with the Auth Helpers client.

Client-side#

Client Components can be used to trigger the authentication process from event handlers.

app/login/page.jsx

_51
'use client'
_51
_51
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
_51
import { useRouter } from 'next/navigation'
_51
import { useState } from 'react'
_51
_51
export default function Login() {
_51
const [email, setEmail] = useState('')
_51
const [password, setPassword] = useState('')
_51
const router = useRouter()
_51
const supabase = createClientComponentClient()
_51
_51
const handleSignUp = async () => {
_51
await supabase.auth.signUp({
_51
email,
_51
password,
_51
options: {
_51
emailRedirectTo: `${location.origin}/auth/callback`,
_51
},
_51
})
_51
router.refresh()
_51
}
_51
_51
const handleSignIn = async () => {
_51
await supabase.auth.signInWithPassword({
_51
email,
_51
password,
_51
})
_51
router.refresh()
_51
}
_51
_51
const handleSignOut = async () => {
_51
await supabase.auth.signOut()
_51
router.refresh()
_51
}
_51
_51
return (
_51
<>
_51
<input name="email" onChange={(e) => setEmail(e.target.value)} value={email} />
_51
<input
_51
type="password"
_51
name="password"
_51
onChange={(e) => setPassword(e.target.value)}
_51
value={password}
_51
/>
_51
<button onClick={handleSignUp}>Sign up</button>
_51
<button onClick={handleSignIn}>Sign in</button>
_51
<button onClick={handleSignOut}>Sign out</button>
_51
</>
_51
)
_51
}

Server-side#

The combination of Server Components and Route Handlers can be used to trigger the authentication process from form submissions.

Sign up route#

app/auth/sign-up/route.js

_24
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
_24
import { cookies } from 'next/headers'
_24
import { NextResponse } from 'next/server'
_24
_24
export async function POST(request) {
_24
const requestUrl = new URL(request.url)
_24
const formData = await request.formData()
_24
const email = formData.get('email')
_24
const password = formData.get('password')
_24
const cookieStore = cookies()
_24
const supabase = createRouteHandlerClient({ cookies: () => cookieStore })
_24
_24
await supabase.auth.signUp({
_24
email,
_24
password,
_24
options: {
_24
emailRedirectTo: `${requestUrl.origin}/auth/callback`,
_24
},
_24
})
_24
_24
return NextResponse.redirect(requestUrl.origin, {
_24
status: 301,
_24
})
_24
}

Login route#

app/auth/login/route.js

_21
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
_21
import { cookies } from 'next/headers'
_21
import { NextResponse } from 'next/server'
_21
_21
export async function POST(request) {
_21
const requestUrl = new URL(request.url)
_21
const formData = await request.formData()
_21
const email = formData.get('email')
_21
const password = formData.get('password')
_21
const cookieStore = cookies()
_21
const supabase = createRouteHandlerClient({ cookies: () => cookieStore })
_21
_21
await supabase.auth.signInWithPassword({
_21
email,
_21
password,
_21
})
_21
_21
return NextResponse.redirect(requestUrl.origin, {
_21
status: 301,
_21
})
_21
}

Logout route#

app/auth/logout/route.js

_15
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
_15
import { cookies } from 'next/headers'
_15
import { NextResponse } from 'next/server'
_15
_15
export async function POST(request) {
_15
const requestUrl = new URL(request.url)
_15
const cookieStore = cookies()
_15
const supabase = createRouteHandlerClient({ cookies: () => cookieStore })
_15
_15
await supabase.auth.signOut()
_15
_15
return NextResponse.redirect(`${requestUrl.origin}/login`, {
_15
status: 301,
_15
})
_15
}

Login page#

app/login/page.jsx

_13
export default function Login() {
_13
return (
_13
<form action="/auth/login" method="post">
_13
<label htmlFor="email">Email</label>
_13
<input name="email" />
_13
<label htmlFor="password">Password</label>
_13
<input type="password" name="password" />
_13
<button>Sign In</button>
_13
<button formAction="/auth/sign-up">Sign Up</button>
_13
<button formAction="/auth/logout">Sign Out</button>
_13
</form>
_13
)
_13
}

Creating a Supabase client#

There are 5 ways to access the Supabase client with the Next.js Auth Helpers:

This allows for the Supabase client to be easily instantiated in the correct context. All you need to change is the context in the middle create[ClientComponent|ServerComponent|ServerAction|RouteHandler|Middleware]Client and the Auth Helpers will take care of the rest.

Client components#

Client Components allow the use of client-side hooks - such as useEffect and useState. They can be used to request data from Supabase client-side, and subscribe to realtime events.

app/client/page.jsx

_20
'use client'
_20
_20
import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'
_20
import { useEffect, useState } from 'react'
_20
_20
export default function Page() {
_20
const [todos, setTodos] = useState()
_20
const supabase = createClientComponentClient()
_20
_20
useEffect(() => {
_20
const getData = async () => {
_20
const { data } = await supabase.from('todos').select()
_20
setTodos(data)
_20
}
_20
_20
getData()
_20
}, [])
_20
_20
return todos ? <pre>{JSON.stringify(todos, null, 2)}</pre> : <p>Loading todos...</p>
_20
}

Singleton#

The createClientComponentClient function implements a Singleton pattern by default, meaning that all invocations will return the same Supabase client instance. If you need multiple Supabase instances across Client Components, you can pass an additional configuration option { isSingleton: false } to get a new client every time this function is called.


_10
const supabase = createClientComponentClient({ isSingleton: false })

Server components#

Server Components allow for asynchronous data to be fetched server-side.

app/page.jsx

_10
import { cookies } from 'next/headers'
_10
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
_10
_10
export default async function Page() {
_10
const cookieStore = cookies()
_10
const supabase = createServerComponentClient({ cookies: () => cookieStore })
_10
const { data } = await supabase.from('todos').select()
_10
return <pre>{JSON.stringify(data, null, 2)}</pre>
_10
}

Server actions#

Server Actions allow mutations to be performed server-side.

app/new-post/page.jsx

_20
import { cookies } from 'next/headers'
_20
import { createServerActionClient } from '@supabase/auth-helpers-nextjs'
_20
import { revalidatePath } from 'next/cache'
_20
_20
export default async function NewTodo() {
_20
const addTodo = async (formData) => {
_20
'use server'
_20
_20
const title = formData.get('title')
_20
const supabase = createServerActionClient({ cookies })
_20
await supabase.from('todos').insert({ title })
_20
revalidatePath('/')
_20
}
_20
_20
return (
_20
<form action={addTodo}>
_20
<input name="title" />
_20
</form>
_20
)
_20
}

Route handlers#

Route Handlers replace API Routes and allow for logic to be performed server-side. They can respond to GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS requests.

app/api/todos/route.js

_11
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
_11
import { NextResponse } from 'next/server'
_11
import { cookies } from 'next/headers'
_11
_11
export async function POST(request) {
_11
const { title } = await request.json()
_11
const cookieStore = cookies()
_11
const supabase = createRouteHandlerClient({ cookies: () => cookieStore })
_11
const { data } = await supabase.from('todos').insert({ title }).select()
_11
return NextResponse.json(data)
_11
}

Middleware#

See refreshing session example above.

Edge runtime#

The Next.js Edge Runtime allows you to host Server Components and Route Handlers from Edge nodes, serving the routes as close as possible to your user's location.

A route can be configured to use the Edge Runtime by exporting a runtime variable set to edge. Additionally, the cookies() function must be called from the Edge route, before creating a Supabase Client.

Server components#

app/page.jsx

_16
import { cookies } from 'next/headers'
_16
import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'
_16
_16
export const runtime = 'edge'
_16
export const dynamic = 'force-dynamic'
_16
_16
export default async function Page() {
_16
const cookieStore = cookies()
_16
_16
const supabase = createServerComponentClient({
_16
cookies: () => cookieStore,
_16
})
_16
_16
const { data } = await supabase.from('todos').select()
_16
return <pre>{JSON.stringify(data, null, 2)}</pre>
_16
}

Route handlers#

app/api/todos/route.js

_15
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs'
_15
import { NextResponse } from 'next/server'
_15
import { cookies } from 'next/headers'
_15
_15
export const runtime = 'edge'
_15
export const dynamic = 'force-dynamic'
_15
_15
export async function POST(request) {
_15
const { title } = await request.json()
_15
const cookieStore = cookies()
_15
const supabase = createRouteHandlerClient({ cookies: () => cookieStore })
_15
_15
const { data } = await supabase.from('todos').insert({ title }).select()
_15
return NextResponse.json(data)
_15
}

Static routes#

Server Components and Route Handlers are static by default - data is fetched once at build time and the value is cached. Since the request to Supabase now happens at build time, there is no user, session or cookie to pass along with the request to Supabase. Therefore, the createClient function from supabase-js can be used to fetch data for static routes.

Server components#

app/page.jsx

_11
import { createClient } from '@supabase/supabase-js'
_11
_11
export default async function Page() {
_11
const supabase = createClient(
_11
process.env.NEXT_PUBLIC_SUPABASE_URL,
_11
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
_11
)
_11
_11
const { data } = await supabase.from('todos').select()
_11
return <pre>{JSON.stringify(data, null, 2)}</pre>
_11
}

Route handlers#

app/api/todos/route.js

_14
import { createClient } from '@supabase/supabase-js'
_14
import { NextResponse } from 'next/server'
_14
_14
export async function POST(request) {
_14
const { title } = await request.json()
_14
_14
const supabase = createClient(
_14
process.env.NEXT_PUBLIC_SUPABASE_URL,
_14
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
_14
)
_14
_14
const { data } = await supabase.from('todos').insert({ title }).select()
_14
return NextResponse.json(data)
_14
}

More examples#

Migration guide#

Migrating to v0.7.X#

PKCE Auth flow#

PKCE is the new server-side auth flow implemented by the Next.js Auth Helpers. It requires a new Route Handler for /auth/callback that exchanges an auth code for the user's session.

Check the Code Exchange Route steps above to implement this Route Handler.

Authentication#

For authentication methods that have a redirectTo or emailRedirectTo, this must be set to this new code exchange Route Handler - /auth/callback. This is an example with the signUp function:


_10
supabase.auth.signUp({
_10
email: 'jon@example.com',
_10
password: 'sup3rs3cur3',
_10
options: {
_10
emailRedirectTo: 'http://localhost:3000/auth/callback',
_10
},
_10
})

Deprecated functions#

With v0.7.x of the Next.js Auth Helpers a new naming convention has been implemented for createClient functions. The createMiddlewareSupabaseClient, createBrowserSupabaseClient, createServerComponentSupabaseClient and createRouteHandlerSupabaseClient functions have been marked as deprecated, and will be removed in a future version of the Auth Helpers.

  • createMiddlewareSupabaseClient has been replaced with createMiddlewareClient
  • createBrowserSupabaseClient has been replaced with createClientComponentClient
  • createServerComponentSupabaseClient has been replaced with createServerComponentClient
  • createRouteHandlerSupabaseClient has been replaced with createRouteHandlerClient

createClientComponentClient returns singleton#

You no longer need to implement logic to ensure there is only a single instance of the Supabase Client shared across all Client Components - this is now the default and handled by the createClientComponentClient function. Call it as many times as you want!


_10
"use client";
_10
_10
import { createClientComponentClient } from "@supabase/auth-helpers-nextjs";
_10
_10
export default function() {
_10
const supabase = createClientComponentClient();
_10
return ...
_10
}

For an example of creating multiple Supabase clients, check Singleton section above.