Home

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.

For a complete implementation example, check out this repo.

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#

npm 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:

1NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
2NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY

Creating a Supabase Client#

Server-side#

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

import { headers, cookies } from 'next/headers'
import { createServerComponentSupabaseClient } from '@supabase/auth-helpers-nextjs'

export const createClient = () =>
  createServerComponentSupabaseClient({
    headers,
    cookies,
  })

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.

Next, we need a middleware file to refresh the user's session on navigation.

If you were using Middleware prior to 12.2, see the upgrade guide.

Create a new middleware.js file at the same level as your app (in the root or src directory) and populate with the following:

import { createMiddlewareSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'

export async function middleware(req) {
  const res = NextResponse.next()

  const supabase = createMiddlewareSupabaseClient({ req, res })

  const {
    data: { session },
  } = await supabase.auth.getSession()

  return res
}

We can now use our server-side Supabase client to fetch data in Server Components.

import 'server-only'

import { createClient } from '../../utils/supabase-server'

// do not cache this page
export const revalidate = 0

export default async function ServerComponent() {
  const supabase = createClient()
  const { data } = await supabase.from('posts').select('*')

  return <pre>{JSON.stringify({ data }, null, 2)}</pre>
}

Client-side#

We still need a Supabase instance client-side for authentication and realtime subscriptions. It is important, when using Supabase client-side, to have a single instance of a client. We can share this singleton instance across our components using providers and React context.

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

import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
export const createClient = () => createBrowserSupabaseClient()

Next, we need to create a single instance of Supabase to use client-side. Let's create a new Provider for Supabase at /components/supabase-provider.jsx and populate with the following:

'use client'

import { createContext, useContext, useState } from 'react'
import { createClient } from '../utils/supabase-browser'

const Context = createContext()

export default function SupabaseProvider({ children }) {
  const [supabase] = useState(() => createClient())

  useEffect(() => {
    const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
      if (session?.access_token !== accessToken) {
        router.refresh()
      }
    })

    return () => subscription.unsubscribe()
  }, [accessToken])

  return (
    <Context.Provider value={{ supabase }}>
      <>{children}</>
    </Context.Provider>
  )
}

export const useSupabase = () => useContext(Context)

We need to set up a listener to fetch fresh data whenever our user logs in or out. For this we need to check whether our client and server sessions match. Let's start by installing the server-only package.

1npm install server-only

This will ensure that any component that imports this package will be a Server Component, and excluded from the browser bundle.

Next, let's modify our root layout to fetch the user's session, wrap our application in our Supabase Provider, and pass the server access token as a prop to the <SupabaseListener /> component (we will create this next).

import 'server-only'

import SupabaseListener from '../components/supabase-listener'
import SupabaseProvider from '../components/supabase-provider'
import './globals.css'
import { createClient } from '../utils/supabase-server'

// do not cache this layout
export const revalidate = 0

export default async function RootLayout({ children }) {
  const supabase = createClient()

  const {
    data: { session },
  } = await supabase.auth.getSession()

  return (
    <html lang="en">
      {/*
      <head /> will contain the components returned by the nearest parent
      head.tsx. Find out more at https://beta.nextjs.org/docs/api-reference/file-conventions/head
    */}
      <head />
      <body>
        <SupabaseProvider>
          <SupabaseListener serverAccessToken={session?.access_token} />
          {children}
        </SupabaseProvider>
      </body>
    </html>
  )
}

And now create our Supabase listener component that uses the singleton Supabase instance to listen for auth changes.

'use client'

import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
import { useSupabase } from './supabase-provider'

export default function SupabaseListener({ serverAccessToken }) {
  const { supabase } = useSupabase()
  const router = useRouter()

  useEffect(() => {
    const {
      data: { subscription },
    } = supabase.auth.onAuthStateChange((event, session) => {
      if (session?.access_token !== serverAccessToken) {
        router.refresh()
      }
    })

    return () => {
      subscription.unsubscribe()
    }
  }, [serverAccessToken, router, supabase])

  return null
}

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 serverAccessToken prop, which is the server's state for our user. If the serverAccessToken and the new session's access_token do not match then the client and server are out of sync, therefore, we want to reload the active route.

Now we can use our useSupabase hook throughout our client-side components.

Authentication#

'use client'

import { useSupabase } from './supabase-provider'

// Supabase auth needs to be triggered client-side
export default function Login() {
  const { supabase, session } = useSupabase()

  const handleEmailLogin = async () => {
    await supabase.auth.signInWithPassword({
      email: 'jon@supabase.com',
      password: 'password',
    })
  }

  const handleGitHubLogin = async () => {
    await supabase.auth.signInWithOAuth({
      provider: 'github',
    })
  }

  const handleLogout = async () => {
    await supabase.auth.signOut()
  }

  return (
    <>
      <button onClick={handleEmailLogin}>Email Login</button>
      <button onClick={handleGitHubLogin}>GitHub Login</button>
      <button onClick={handleLogout}>Logout</button>
    </>
  )
}

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:

'use client'

import { useEffect, useState } from 'react'
import { useSupabase } from '../../components/supabase-provider'

export default function Posts({ serverPosts }) {
  const [posts, setPosts] = useState(serverPosts)
  const { supabase } = useSupabase()

  useEffect(() => {
    setPosts(serverPosts)
  }, [serverPosts])

  useEffect(() => {
    const channel = supabase
      .channel('*')
      .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts' }, (payload) =>
        setPosts((posts) => [...posts, payload.new])
      )
      .subscribe()

    return () => {
      supabase.removeChannel(channel)
    }
  }, [supabase, setPosts, posts])

  return <pre>{JSON.stringify(posts, null, 2)}</pre>
}

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:

import 'server-only'

import { createClient } from '../../utils/supabase-server'
import Posts from './posts'

// do not cache this page
export const revalidate = 0

export default async function Realtime() {
  const supabase = createClient()
  const { data } = await supabase.from('posts').select('*')

  return <Posts serverPosts={data || []} />
}
Need some help?

Not to worry, our specialist engineers are here to help. Submit a support ticket through the Dashboard.