Home

Supabase Auth with Remix

This submodule provides convenience helpers for implementing user authentication in Remix applications.

For a complete implementation example, check out this free egghead course or this GitHub repo.

Install the Remix helper library#

npm install @supabase/auth-helpers-remix

This library supports the following tooling versions:

  • Remix: >=1.7.2

Set up environment variables#

Retrieve your project URL and anon key in your project's API settings in the Dashboard to set up the following environment variables. For local development you can set them in a .env file. See an example.

1SUPABASE_URL=YOUR_SUPABASE_URL
2SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY

Server-side#

The Supabase client can now be used server-side - in loaders and actions - by calling the createServerClient function.

Loader#

Loader functions run on the server immediately before the component is rendered. They respond to all GET requests on a route. You can create an authenticated Supabase client by calling the createServerClient function and passing it your SUPABASE_URL, SUPABASE_ANON_KEY, and a Request and Response.

import { json } from '@remix-run/node' // change this import to whatever runtime you are using
import { createServerClient } from '@supabase/auth-helpers-remix'

export const loader = async ({ request }) => {
  const response = new Response()
  // an empty response is required for the auth helpers
  // to set cookies to manage auth

  const supabaseClient = createServerClient(
    process.env.SUPABASE_URL,
    process.env.SUPABASE_ANON_KEY,
    { request, response }
  )

  const { data } = await supabaseClient.from('test').select('*')

  // in order for the set-cookie header to be set,
  // headers must be returned as part of the loader response
  return json(
    { data },
    {
      headers: response.headers,
    }
  )
}

Supabase will set cookie headers to manage the user's auth session, therefore, the response.headers must be returned from the Loader function.

Action#

Action functions run on the server and respond to HTTP requests to a route, other than GET - POST, PUT, PATCH, DELETE etc. You can create an authenticated Supabase client by calling the createServerClient function and passing it your SUPABASE_URL, SUPABASE_ANON_KEY, and a Request and Response.

import { json } from '@remix-run/node' // change this import to whatever runtime you are using
import { createServerClient } from '@supabase/auth-helpers-remix'

export const action = async ({ request }) => {
  const response = new Response()

  const supabaseClient = createServerClient(
    process.env.SUPABASE_URL,
    process.env.SUPABASE_ANON_KEY,
    { request, response }
  )

  const { data } = await supabaseClient.from('test').select('*')

  return json(
    { data },
    {
      headers: response.headers,
    }
  )
}

Supabase will set cookie headers to manage the user's auth session, therefore, the response.headers must be returned from the Action function.

Session and User#

You can determine if a user is authenticated by checking their session using the getSession function.

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

The session contains a user property.

const user = session?.user

Or, if you don't need the session, you can call the getUser() function.

const {
  data: { user },
} = await supabaseClient.auth.getUser()

Client-side#

We still need to use Supabase client-side for things like authentication and realtime subscriptions. Anytime we use Supabase client-side it needs to be a single instance.

Creating a singleton Supabase client#

Since our environment variables are not available client-side, we need to plumb them through from the loader.

export const loader = () => {
  const env = {
    SUPABASE_URL: process.env.SUPABASE_URL,
    SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY,
  }

  return json({ env })
}

These may not be stored in process.env for environments other than Node.

Next, we call the useLoaderData hook in our component to get the env object.

const { env } = useLoaderData()

We then want to instantiate a single instance of a Supabase browser client, to be used across our client-side components.

const [supabase] = useState(() => createBrowserClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY))

And then we can share this instance across our application with Outlet Context.

<Outlet context={{ supabase }} />

Syncing server and client state#

Since authentication happens client-side, we need to tell Remix to re-call all active loaders when the user signs in or out.

Remix provides a hook useRevalidator that can be used to revalidate all loaders on the current route.

Now to determine when to submit a post request to this action, we need to compare the server and client state for the user's access token.

Let's pipe that through from our loader.

export const loader = async ({ request }) => {
  const env = {
    SUPABASE_URL: process.env.SUPABASE_URL,
    SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY,
  }

  const response = new Response()

  const supabase = createServerClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY, {
    request,
    response,
  })

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

  return json(
    {
      env,
      session,
    },
    {
      headers: response.headers,
    }
  )
}

And then use the revalidator, inside the onAuthStateChange hook.

const { env, session } = useLoaderData()
const { revalidate } = useRevalidator()

const [supabase] = useState(() => createBrowserClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY))

const serverAccessToken = session?.access_token

useEffect(() => {
  const {
    data: { subscription },
  } = supabase.auth.onAuthStateChange((event, session) => {
    if (session?.access_token !== serverAccessToken) {
      // server and client are out of sync.
      revalidate()
    }
  })

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

Check out this repo for full implementation example

Authentication#

Now we can use our outlet context to access our single instance of Supabase and use any of the supported authentication strategies from supabase-js.

export default function Login() {
  const { supabase } = useOutletContext()

  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>
    </>
  )
}

Subscribe to realtime events#

import { useLoaderData, useOutletContext } from '@remix-run/react'
import { createServerClient } from '@supabase/auth-helpers-remix'
import { json } from '@remix-run/node'
import { useEffect, useState } from 'react'

export const loader = async ({ request }) => {
  const response = new Response()
  const supabase = createServerClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY, {
    request,
    response,
  })

  const { data } = await supabase.from('posts').select()

  return json({ serverPosts: data ?? [] }, { headers: response.headers })
}

export default function Index() {
  const { serverPosts } = useLoaderData()
  const [posts, setPosts] = useState(serverPosts)
  const { supabase } = useOutletContext()

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

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

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

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

Ensure you have enabled replication on the table you are subscribing to.