Supabase Auth with Next.js Server Components

This submodule provides experimental convenience helpers for implementing user authentication in Next.js Server Components - the app directory. For examples using the pages directory check out Auth Helpers in Next.js.

To learn more about fetching and caching Supabase data with Next.js 13 Server Components, check out our blog or live stream.

Install the Next.js helper library#

1npm install @supabase/auth-helpers-nextjs

Next.js Server Components and the app directory are experimental and likely to change.

Set up environment variables#

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

".env.local"
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY

Creating a Supabase Client#

Create a new file at /utils/supabase-browser.js and populate with the following:

"/utils/supabase-browser.js"
1import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
2
3export default createBrowserSupabaseClient()

This will be used any time we need to create a Supabase client client-side - in useEffect, for example.

Create a new file at /utils/supabase-server.js and populate with the following:

"/utils/supabase-server.js"
1import { headers, cookies } from 'next/headers'
2import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'
3
4export default () =>
5  createServerComponentSupabaseClient({
6    headers,
7    cookies,
8  })

This needs to export a function, as the headers and cookies are not populated with values until the Server Component is requesting data.

This will be used any time we need to create a Supabase client server-side - in a Server Component, for example.

Middleware#

Middleware runs before every route declared in the matcher array. Since we don't have access to set cookies or headers from Server Components, we need to create a Middleware Supabase client and refresh the user's session by calling getSession().

"middleware.js"
1import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
2import { NextResponse } from 'next/server'
3
4export async function middleware(req) {
5  const res = NextResponse.next()
6
7  const supabase = createMiddlewareSupabaseClient({ req, res })
8
9  const {
10    data: { session },
11  } = await supabase.auth.getSession()
12
13  return res
14}
15
16export const config = {
17  matcher: ['/optional-session', '/required-session', '/realtime'],
18}

Any Server Component route that uses a Supabase client must be added to this middleware's matcher array. Without this, the Server Component may try to make a request to Supabase with an expired access_token.

Supabase Listener#

We need to set up a listener to fetch fresh data whenever our user logs in or out.

Create a /components/supabase-listener.jsx file and add the following:

"/components/supabase-listener.jsx"
1'use client'
2
3import { useRouter } from 'next/navigation'
4import { useEffect } from 'react'
5import supabase from '../utils/supabase'
6
7export default function SupabaseListener({ accessToken }) {
8  const router = useRouter()
9
10  useEffect(() => {
11    supabase.auth.onAuthStateChange((event, session) => {
12      if (session?.access_token !== accessToken) {
13        router.refresh()
14      }
15    })
16  }, [accessToken])
17
18  return null
19}

use client tells Next.js that this is a Client Component. Only Client Components can use hooks like useEffect and useRouter.

The function we pass to onAuthStateChange is automatically called by Supabase whenever a user's session changes. This component takes an accessToken prop, which will be the server's state for our user (we'll set this up next). If the accessToken from the server and the new access_token do not match then the client and server are out of sync, therefore, we want to reload the active route.

Lastly, fetch the server-side session in the RootLayout and pass it to our new <SupabaseListener /> component.

"/app/layout.jsx"
1import SupabaseListener from '../components/supabase-listener'
2import createClient from '../utils/supabase-server'
3
4export default async function RootLayout({ children }) {
5  const supabase = createClient()
6
7  const {
8    data: { session },
9  } = await supabase.auth.getSession()
10
11  return (
12    // html and head section omitted
13    <body>
14      <SupabaseListener accessToken={session?.access_token} />
15      {children}
16    </body>
17  )
18}

We don't want Next.js to cache this session value, so we need to export a revalidate value of 0.

1export const revalidate = 0

We also want to tell Next.js to explicitly exclude this component's code from the client bundle by installing the server-only package:

npm install server-only

And importing it at the top of our component.

1import 'server-only'

The entire Layout component should look something like this:

"/app/layout.jsx"
1import 'server-only'
2
3import SupabaseListener from '../components/supabase-listener'
4import './globals.css'
5import createClient from '../utils/supabase-server'
6
7// do not cache this layout
8export const revalidate = 0
9
10export default async function RootLayout({ children }) {
11  const supabase = createClient()
12
13  const {
14    data: { session },
15  } = await supabase.auth.getSession()
16
17  return (
18    <html lang="en">
19      {/*
20        <head /> will contain the components returned by the nearest parent
21        head.jsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head
22      */}
23      <head />
24      <body>
25        <SupabaseListener accessToken={session?.access_token} />
26        {children}
27      </body>
28    </html>
29  )
30}

Supabase can now be used in any Client or Server component.

Server Components#

Next.js recommends fetching data in Server Components whenever possible.

1import 'server-only'
2
3import createClient from '../../utils/supabase-server'
4
5// do not cache this page
6export const revalidate = 0
7
8export default async function ServerComponent() {
9  const supabase = createClient()
10  const { data } = await supabase.from('posts').select('*')
11
12  return <pre>{JSON.stringify({ data }, null, 2)}</pre>
13}

Client Components#

While Next.js recommend doing all data fetching in Server Components, we still need Supabase client-side for things like authentication and subscribing to realtime updates.

Authentication#

We can call any of Supabase's authentication methods - such as supabase.auth.signInWithOAuth - from a client component.

"/components/login.jsx"
1'use client'
2
3import supabase from '../utils/supabase-browser'
4
5export default function Login() {
6  const handleLogin = async () => {
7    const { error } = await supabase.auth.signInWithOAuth({
8      provider: 'github',
9    })
10
11    if (error) {
12      console.log({ error })
13    }
14  }
15
16  const handleLogout = async () => {
17    const { error } = await supabase.auth.signOut()
18
19    if (error) {
20      console.log({ error })
21    }
22  }
23
24  return (
25    <>
26      <button onClick={handleLogin}>Login</button>
27      <button onClick={handleLogout}>Logout</button>
28    </>
29  )
30}

Realtime#

A nice pattern for fetching data server-side and subscribing to changes client-side can be done by combining Server and Client components.

To receive realtime events, you must enable replication on your "posts" table in Supabase.

Create a new file at /app/realtime/posts.jsx and populate with the following:

"/app/realtime/posts.jsx"
1'use client'
2
3import { useEffect, useState } from 'react'
4import supabase from '../../utils/supabase-browser'
5
6export default function Posts({ serverPosts }) {
7  const [posts, setPosts] = useState(serverPosts)
8
9  useEffect(() => {
10    setPosts(serverPosts)
11  }, [serverPosts])
12
13  useEffect(() => {
14    const channel = supabase
15      .channel('*')
16      .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts' }, (payload) =>
17        setPosts((posts) => [...posts, payload.new])
18      )
19      .subscribe()
20
21    return () => {
22      supabase.removeChannel(channel)
23    }
24  }, [serverPosts])
25
26  return <pre>{JSON.stringify(posts, null, 2)}</pre>
27}

The first useEffect is required to overwrite posts if the serverPosts prop changes.

This can now be used in a Server Component to subscribe to realtime updates.

Create a new file at /app/realtime/page.jsx and populate with the following:

"/app/realtime/page.jsx"
1import 'server-only'
2
3import createClient from '../../utils/supabase-server'
4import Posts from './posts'
5
6// do not cache this page
7export const revalidate = 0
8
9export default async function Realtime() {
10  const supabase = createClient()
11  const { data } = await supabase.from('posts').select('*')
12
13  return <Posts serverPosts={data || []} />
14}