Authentication in Next.js with Supabase and Next 13
Note that this guide is now outdated, and is using an older version of the Next.js Auth Helpers (pinned at
0.6.1). Please see the updated guide for the latest version that utilizes the Proof Key for Code Exchange (PKCE) flow.
Awhile back I wrote an article on using Supabase to implement user authentication in a Next.js app. As it often goes in the world of open-source web, things evolve quickly and a lot can change in just over a year, and so is the case with Supabase.
In this post, I would like to bring you an updated guide on implementing user auth with Supabase and Next.js in 2023.
NOTE: Make sure to read the original post first if you haven't yet, as we'll be building on the main concepts covered there. Code for the Supabase v1 implementation can be found under the
v1branch in GitHub.
So, what all has changed? Let's start with the big stuff.
Though officially still in beta (as of January 2023), the
app directory offers a great new way of architecting our apps, and introduces new features like nested layouts and support for React Server Components.
I wanted to explore how this new paradigm would work with Supabase, and try some of the capabilities in a familiar project. Happy to say that this exploration went well, and we'll go over one approach of using the new
app architecture in this guide.
Supabase Auth Helpers
In addition to the new version of the client library, Supabase also introduced new Auth Helpers earlier this year, which is a collection of libraries and framework-specific utilities for working with user authentication in Supabase.
These utilities eliminate the need to keep writing the same boilerplate code over and over (e.g. initializing Supabase client, setting cookies, etc.), and let us focus on our application code instead. We'll be utilizing their Next.js helpers library in our project.
Supabase UI (Deprecated)
Auth component from the Supabase UI library which we were using to render our auth screens, and handle all the related flows and UI logic (Sign In, Sign Up, Reset Password, etc.) has been deprecated. Components from the original @supabase/ui package were moved into the main Supabase repo, and the package is now deprecated.
The Auth component itself now has a new name "Auth UI", and lives in its own separate repo. Though originally intending to use it, as I began migrating the code I've found this new component to not work quite as well as I'd hoped, getting in the way more than helping. For that reason, I've decided to abandon it in this guide, and build one instead.
Fortunately, building a component like this from scratch isn't all that difficult thanks to libraries like Formik, so we'll use that to help us handle all of our form logic. This has the added benefit of giving us full control of the auth flow, and the ability to customize the form UI however we'd like, without being limited by Supabase's customization options.
In the interest of saving some time, we'll start with the project from the original post, as we do end up re-using a bunch of existing code.
The code for that is in GitHub for your reference. If you're doing this for the first time, clone the
v1 branch as your starting point.
We'll also utilize Tailwind's Forms Plugin to make it easier to style our
Auth component, so let's install that as well:
You can also remove
@supabase/ui, since we won't be using it anymore:
With dependencies updated, we'll need to configure Next to use the new
app directory (make sure you're on version 13 or above). To enable this, add (or update)
next.config.js in the project's root folder:
This will allow us to start moving some of our existing code from the
pages directory to the
app folder. Note that putting the
app folder under
src also works, which is how the previous project was setup.
Make sure to read more about the new
appdirectory if you aren't familiar with the new features.
Updating Tailwind Config
Last thing we need to do is specify the
@tailwindcss/forms plugin in our Tailwind config file:
Supabase API keys
To use Supabase, we'll need to have our Supabase API key and URL setup.
With API keys setup, let's now create the Supabase Client so that we can interact with Supabase API's. We'll be using the Next.js Auth Helpers library installed earlier for this.
The instructions below are using
@firstname.lastname@example.org. At the time of writing, there is an issue with password reset flow in the latest version. Additionally, some of the methods used in this guide have been renamed in version
0.7.0. See the project repo in GitHub for any future changes.
In order to support both the new Server Components and the Client Components, we'll need to have two different kinds of a Supabase Client:
- Browser Client: for use with Client Components in the browser, for example within a
- Server Component Client: for use specifically with Server Components
Create two files -
supabase-server.js - one for each type of client. We'll put these in the
Server Component Client
Note that this needs to export a function, as the
cookies are not populated with values until the Server Component is requesting data (according to Supabase docs).
With the clients setup, we can now import them in our pages and components to interact with Supabase.
E.g. in a Client Component:
E.g. in a Server Component:
The overall page structure and routes will largely remain the same as before, so we'll get to re-use a bunch of existing code. The biggest change will be moving all page code from the
pages folder to the new
app directory. As we do that, we'll also need to refactor some things to align with this new paradigm.
Our project folder structure will look like the following:
Let's go through what each of these are.
Pages and Routes
This is the main Home page, showing the
Auth component if no session is found (ie. user needs to sign-in), or a link to the Profile page if there is an active session with a valid user.
This page also handles the "Update Password" flow, and will be redirected to from the link in the reset password email sent by Supabase (this URL is configurable in case you'd like to use a different route).
Previously in src/pages/index.js, this page will now be in
This is an authenticated Profile page, showing some basic user info for the current user. If no session is found, it will redirect to
Previously in src/pages/profile.js, this page code will now be in
And that's it as far as page routes go.
In the previous solution, we built our own Layout component, and used it as the top-level wrapper for each individual page, ie:
With Next 13 and the
app directory, this can now be done using shared layouts ✨
Since we only have a single layout shared across the whole app, we can just create a single root
layout.js file in
src/app, which will be shared by all children routes and pages. We'll call this
Using the existing Layout component as our starting point, create
src/app/layout.js and paste the following (feel free to adjust styling as needed):
Note how we need to include
<body> tags here as well. If you'd like to think of it in terms of the old
pages directory, the root
layout.js essentially combines the concepts of
_document.js together into one.
One big benefit of a root layout is that state is preserved on route changes, and the layout doesn't unnecessarily rerender. Layouts can also be nested, though we won't be needing that in this project.
IMPORTANT: Root layout is a Server Component by default, and can NOT be set to a Client Component. Any parts of your layout that require interactivity will need to be moved into separate Client Components (which can be marked with the
'use client' directive).
Read more on this in "moving Client Components to the leaves" in Next.js docs for additional information.
This component is a top-level wrapper in our application, providing things like
signOut method and a
useAuth hook to its children components.
Previously found in the
src/lib/auth.js file in the original implementation, we'll move this to
src/components/AuthProvider.js to keep our folder and naming structure consistent. For now, keep all of the existing code - we'll update it as we go along.
In the original implementation, this component was used in the
With the move to
app directory, we'll need to find it a new home. Considering that this will be shared by all pages, the best place for it is in the
src/app/layout.js with the following:
One important thing to note from Next.js beta docs:
In Next.js 13, context is fully supported within Client Components, but it cannot be created or consumed directly within Server Components. This is because Server Components have no React state (since they're not interactive), and context is primarily used for rerendering interactive components deep in the tree after some React state has been updated.
Given the above, we'll need to make
AuthProvider a Client Component, since it uses React context.
To do this, add
'use client' at the very top of the
Note that even though
AuthProvider is now a Client Component, it can still be imported and used in the
RootLayout (which is a Server Component).
Next.js middleware is used to run code before a request is completed, and can be configured to run that code only on specific routes. From Supabase docs we learn that:
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(). Any Server Component route that uses a Supabase client must be added to this middleware's
matcherarray. Without this, the Server Component may try to make a request to Supabase with an expired
So, we'll need to add Next.js middleware in our app. Create a
middleware.js file at the root of our
src folder, and add the following:
Here we're using the
createMiddlewareSupabaseClient() function from the Next.js Auth Helpers to create a middleware Supabase client, and we're also configuring the
matcher array to run this code on the
/profile route, as that's the only route using Server Components in our case. If you have additional routes utilizing Server Components, you'll need to specify them here as well.
Migrating to Supabase v2
With the core of our
app code now in place, it's time to make some updates.
There were a few methods deprecated in Supabase v2, so we'll need to update how we use those in our
If you recall, we have a
useEffect inside our
AuthProvider that retrieves the current session, as well as an event listener for any auth events fired by Supabase client.
The methods for these have changed in Supabase v2, so we'll need to update how they're used.
First, remove the deprecated
session() method and use getSession() instead.
Because this new method returns a Promise, we need to call it from within an async function in our
Create an async
getActiveSession method, and invoke it from within the
useEffect. In the
AuthProvider component, update the
useEffect from this:
Next, we need to update the
To get the
authListener for our effect cleanup, we now need to read it from
data.subscription in the returned object:
getUserByCookie methods have also been deprecated. Recommended solution for managing cookies in Next.js is now the Next.js Auth Helpers library, which we had installed earlier. We'll be using that alongside Next.js middleware (more on that below).
This means that we won't need to manually create or delete a cookie whenever auth state changes, and the
/api/auth API routes are no longer required. We can delete
pages/api/auth altogether, and remove the fetch call to
/api/auth in our
onAuthStateChange handler as well:
AuthProvider should now look like this:
With Supabase setup, and
AuthProvider updated to use Supabase v2, it's time to build our authentications forms. This is what will replace the Auth component from the (now deprecated) Supabase UI library.
Let's make each form an individual component:
- Sign In:
- Sign Up:
- Reset Password:
- Update Password:
Then, create a parent
Auth component that will display the corresponding screen based on the current
Here, we're checking the value of
view from our
AuthProvider via the
useAuth() hook, and display the corresponding component for the given auth flow. The
Auth component also accepts an optional
view prop, in case we need to manually override it (as is the case with "Update Password" flow, but more on that later).
The individual forms all follow the same basic structure, which looks like this for Sign In:
Check out the full Formik tutorial for more details on how it works.
Similarly, the remaining auth flows are also implemented:
Check out the linked files for reference.
We'll put all these in the
src/components/Auth folder, where
index.js is the parent
Auth component, and then each individual form is a separate component file:
Code for the complete
Authcomponent can be found in GitHub for your reference.
With the new
Auth component in place, it's time to update our page code.
Our Home page will use the original code from src/pages/index.js as the starting point. And there really isn't much to change!
- We're still using the
useAuthhook, and reading
- Thanks to the new shared layout, we don't need the
Authcomponent API is a bit simplified, as it doesn't require the Supabase client to be passed to it anymore (that's done internally in the component)
- The imports for
Authwill also need to be updated
With all said, the
Home page should look something like this:
As before, we are showing the
Auth component if no
user is found in the current session, or a simple authenticated view with a link to
/profile and a Sign Out button otherwise.
Note also that we are rendering the
Auth component first if the
UPDATE_PASSWORD, which means that a user has been redirected to here after clicking the link in Supabase Reset Password email.
NOTE: It's important that this is returned for the
UPDATE_PASSWORDview regardless if there's an active
user, as the email link passes an
access_tokenalong with the URL and Supabase client creates a
sessionwith this token. So we're basically in an "authenticated" state during the Update Password flow, and if we check for a
userfirst, the home page will always show the authenticated view without giving our user ability to update their password.
This is essentially the same behaviour as we had in the original solution, but updated to use the new
Now, if we try to run the app and go to the Home page in its current state, we'll get an error like this:Home page: Error
As mentioned earlier, in Next 13 pages are Server Components by default when using the
app dir. This means that our
AuthProvider, which is a Client Component, cannot be consumed within the Home page in its current state.
If we look at the error a bit closer we'll see that the
useAuth hook is the culprit:
To fix this, we need to make
Home a Client Component. As before, add
'use client' at the very top of the
Now if we run the app again, we should see the
Auth component rendered:
Clicking on "Forgot password" or "Sign Up" text will set
view to the corresponding screen (this is implemented internally in the
Auth component, by calling
setView from the
Let's go ahead and either Sign In or Sign Up. These flows behave the same as before, and if successful we should see the authenticated part of our Home page:Home page: Authenticated view
The view rerendered because
Home page is a Client Component and is nested within the
AuthProvider (also a Client Component). As we had previously done, the
onAuthStateChange listener will listen for auth event changes, and update the
view in the state, thereby triggering a rerender in relevant components (the
Home page in this case).
For the Profile page, we'll be starting off with src/pages/index.js as our base.
We'll keep this page as a Server Component (default in Next 13), which means that we can call API's and server-side methods directly in the component. So any calls made within
getServerSideProps before can now be done directly in the component.
This is also where we use the Supabase Server Component client we had created in
Add the following to
Let's break this down a bit.
As noted earlier, the
getUserByCookie method was deprecated. We can now get the current user with the
Notice that because
getUser() is an async method, we need to make
Profile and asynchronous component as well. Keep this in mind as we continue.
Next, we check if there's a valid
user, and if there isn't - redirect user back to Home using the redirect function from Next.js. This keeps the Profile page protected, and only allow authenticated users to access it.
redirect()can only be called as part of rendering and not as part of event handlers (source). This means you can't use
redirectwithin a button's
onClickhandler, for example.
Let's run our app, and make sure everything works as expected. If signed in, we should see our Profile page:Profile
Everything works great!
But let's say we want to add a "Sign Out" button:Profile
This button will need an
onClick handler, which means that we can't use it directly in the
Profile as that's a Server Component. We'll need to make this button into a separate Client Component, which can then be imported and used in the
Let's put this tiny component in
src/components/SignOut.js. Upon clicking the button, it'll call the
signOut() method from our
Then add it to the Profile page:
Now when user presses the button, they'll be signed out and
session cleared. But there's one problem.
You see, since
Profile is a Server Component, any updates in the
AuthProvider state don't cause it to rerender. This also means that any changes in Supabase auth state (ie. user signing out) don't trigger a rerender either, and so our UI doesn't reflect the change in the auth state. We need a different way of handling this, compared to Client Components.
Syncing-up Server and Client states
Our problem is that after a user signs out, our UI (ie. the rendered Server Component) and server state are no longer in sync. In order for them to be in sync, we need to rerender our page.
One way of doing that is to simply reload the page. In fact, if you reload the browser on
/profile again, it should redirect you back to Home, as intended. Understandably, we shouldn't be relying solely on users manually refreshing their browser window to update our UI state.
Thanks to the new router in Next 13, we can use the useRouter hook and call the
router.refresh() method to trigger a route refresh when there is no longer a valid user in the session.
But how do we know if the session is no longer valid on the server side? A-ha! For this, we'll need to check whether our client and server sessions match.
AuthProvider, add the following:
Now, whenever the auth state changes, we're checking if the current session's
access_token (on the client) matches the one on the server. If it does not, we trigger a
router.refresh() and our UI state will be updated. The server's access token will be passed as the
So, where should this access token come from? Well, considering that we need to pass it to the
AuthProvider and it needs to be done server-side, we need to fetch it in our
Add the following to
This will read the current server-side
session and pass its
access_token as the
accessToken prop to the
AuthProvider. Now the auth listener will have something to compare the client-side session to.
Add the following to
This will ensure that every time a new route is loaded, our
session data in
RootLayout will always be up-to-date.
Now if go back to the Profile page, and click "Sign Out", we should be redirected to Home page.
Adding loading state
You may have noticed that when going to Profile for the first time, it takes a little bit of time to load. Let's exaggerate it by adding a simple
sleep util in our
This will add a 2-second delay to the
getUser() call. Now that is really noticeable. Let's make this better.
Using it couldn't be any simpler. Just add a
loading.js file wherever you'd like to create an instant loading state, and specify the UI to show.
For our Profile, let's add the following to
Now, when you reload the page (or go to
/profile from the home page), you should see the loading UI almost immediately:
And now that we've verified it works as expected, let's not forget to remove that
sleep call from our Profile page:
Flash of unauthenticated state
Almost done! But before we wrap up, there is one other thing left to fix.
Right now, if we have a valid
session and reload the Home page, we'll still see a brief flash of unauthenticated state:
This happens because on the very first render the
Home page (which remember is a Client Component) doesn't yet have any
session data from the
AuthProvider, so it returns the
This, of course, is desired behaviour when there is no user data found. But we know that our
session data has a valid user - it just so happens to not be available on the first render, since we fetch that data inside a
useEffect in our
To fix this, we need to be able to differentiate between an "initial" state of our app on the client side - before we check if there's a valid session - and after. This way we'll know whether a valid
session truly doesn't exist, or just hasn't had a chance to load yet.
Home was a Server Component, then we could simply
await the result of
getSession() (like we do in the
RootLayout) and place a Suspense boundary to show a loading state (like we did for the Profile above). But because
Home is a Client Component, that won't work.
You see, root-level
await is not supported in client-side components, and so we can't "suspend" our Home page until the
session data is available like we do with Profile. React team is working on an RFC and a new use hook that will allow us to conditionally wait for data to load, but it's not quite ready yet. Its use (pardon the pun) is also not recommended by Next itself, as it may cause multiple re-renders in Client Components. Considering all that, we'll need to find an alternative way.
There are a few ways we could solve this. For example, we could move the "authenticated" portion of our Home page to a new route altogether (e.g.
/home), thereby completely separating the "unathenticated" state. But that would involve a bunch of refactoring of the code we already wrote, and ideally would like to avoid that for this guide. So in the interest of time, we'll go with a bit more "old school" approach.
One simple way to fix this is to add another state variable in our
AuthProvider that will simply tell us whether our app is loading for the first time or not. We can set its value to
true initially, and once we do the first call to
supabase.auth.getSession() we can set it to
false, indicating that our app has loaded the data.
AuthProvider, create an
initial state, and make sure its value is provided:
Now we can read
initial using the
useAuth hook in our Home page, and while it's value is
true, show our loading state instead of returning the
And now if we reload our Home page, we should see the same kind of loading state that we have in Profile:Home Page loading
This is a quick and easy way of dealing with this problem, but by no means the only way. Ideally we'd like to use Suspense for this, but until React lands on a more established pattern for it in client-side components, we're not going to focus on it too much. For the time being, this solves our immediate problem in this scenario.
Well, this about wraps it up. If you've made it this far - thank you for reading! Hopefully this guide gives you a good starting point for using Supabase in your Next 13 project, or at the very least points you in the right direction.
Make sure to give the new Next.js Beta Docs a read as well, as I've found them to be an invaluable resource for learning some of these new paradigms coming to the world of React.~~
EDIT: The new Next.js Docs have now been updated. Definitely give them a read if you haven't yet.