Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Next.js 13 app directory support #341

Closed
thorwebdev opened this issue Oct 26, 2022 · 47 comments · Fixed by #380
Closed

Next.js 13 app directory support #341

thorwebdev opened this issue Oct 26, 2022 · 47 comments · Fixed by #380
Labels
enhancement New feature or request nextjs Next.js specific functionality

Comments

@thorwebdev
Copy link
Member

thorwebdev commented Oct 26, 2022

With version v0.5.2 we've added a createServerComponentSupabaseClient which can be used server components to get an authenticated Supabase client. This is the PR which includes an example. Note that this needs to be used in combination with middleware as Next.js does not yet allow setting headers in server components.

Docs to follow soon.

@Mar0xy
Copy link

Mar0xy commented Oct 26, 2022

E.g. currently it isn't clear how to access what used to be ctx.res in getServerSideProps

Seems like context was generally replaced in favor of the headers and cookies module as it stands now you can only read from both but according to Next.js they are planning on implementing a set function for cookies so you can set cookies.

"The Next.js team at Vercel is working on adding the ability to set cookies in addition to the cookies function. For now, if you need to set cookies, you can use Middleware."

@Murkrage

This comment was marked as outdated.

@jensen

This comment was marked as outdated.

@thorwebdev
Copy link
Member Author

Thanks @jensen I actually think pairing it with middleware is the right approach because the middleware can modify the response to refresh the session and update the cookie if needed: https://github.com/supabase/auth-helpers/blob/main/packages/nextjs/src/middleware/withMiddlewareAuth.ts#L65-L72

@jensen
Copy link

jensen commented Oct 27, 2022

Yeah, I was thinking the same thing.

I'd like to look at the middleware in more detail. We need access to the request, and the response in order to create the storage callbacks, that part is reasonable. The part that I am unsure of is how we pass a supabase instance to a server component. We can access cookies and headers at this point but it seems like no complex types.

Would love to be wrong on this.

@The-Code-Monkey

This comment was marked as outdated.

@jensen

This comment was marked as outdated.

@Murkrage

This comment was marked as outdated.

@The-Code-Monkey

This comment was marked as off-topic.

@jensen
Copy link

jensen commented Oct 29, 2022

Just a heads up for anyone working on Next 13. I found out the hard way that leaving out a pages folder will prevent middleware.ts from loading. I had to add a pages/.keep file for now, and it works. I was going to add an issue, but found this one vercel/next.js#41995 was already submitted.

@rodzerjohn

This comment was marked as off-topic.

@jensen

This comment was marked as outdated.

@Murkrage

This comment was marked as outdated.

@The-Code-Monkey

This comment was marked as off-topic.

@The-Code-Monkey

This comment was marked as outdated.

@kentare

This comment was marked as outdated.

@The-Code-Monkey

This comment was marked as off-topic.

@kentare

This comment was marked as off-topic.

@The-Code-Monkey

This comment was marked as off-topic.

@The-Code-Monkey

This comment was marked as off-topic.

@nicolazj

This comment was marked as outdated.

@thorwebdev thorwebdev changed the title Next.js 13 support Next.js 13 app directory support Nov 3, 2022
@Mar0xy
Copy link

Mar0xy commented Nov 3, 2022

If this PR gets merged and put into canary for the next version of next.js I think it would then enable setting cookies through request (would be nice if they would also export ResponseCookies).

@hjaber

This comment was marked as off-topic.

@thorwebdev
Copy link
Member Author

With version v0.5.2 we've added a createServerComponentSupabaseClient which can be used server components to get an authenticated Supabase client. This is the PR which includes an example. Note that this needs to be used in combination with middleware as Next.js does not yet allow setting headers in server components.

Docs to follow soon.

@thorwebdev thorwebdev linked a pull request Nov 15, 2022 that will close this issue
@rodzerjohn
Copy link

How do i call inner nextjs api with session? session object in middleware returns null.

@yinkakun
Copy link

How do i call inner nextjs api with session? session object in middleware returns null.

Same for me too.

@walton-alex
Copy link

Is there an ETA on the docs at all?

@dijonmusters
Copy link
Contributor

dijonmusters commented Nov 17, 2022

How do i call inner nextjs api with session? session object in middleware returns null.

Very strange. This should only happen if you are not signed in.

If you make your middleware something like this:

import { createMiddlewareSupabaseClient } from "@supabase/auth-helpers-nextjs";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export async function middleware(req: NextRequest) {
  const res = NextResponse.next();
  const supabase = createMiddlewareSupabaseClient({ req, res });

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

  console.log({ session });

  return res;
}

export const config = {
  matcher: ["/api/hello"],
};

And call one of the Supabase auth methods client-side, for example:

await supabase.auth.signInWithPassword({
      email: 'jon@supabase.com',
      password: 'password'
    });

Do you still get a null session when navigating to your API route: /api/hello?

@dijonmusters
Copy link
Contributor

Is there an ETA on the docs at all?

Aiming to get these live today 👍

@tiniscule
Copy link

tiniscule commented Nov 18, 2022

Do you still get a null session when navigating to your API route: /api/hello?

Yes - it happens on the first load after an auth. The session doesn't get hydrated until the client interacts with it once - then a reload will show you it in the middleware. But the first middleware hit after authentication always returns a null session for me.

@thorwebdev
Copy link
Member Author

But the first middleware hit after authentication always returns a null session for me.

Unfortunately that's to be expected for Oauth (redirect) based methods. The tokens are communicated via query fragments which cannot be accessed on the server. Therefore the client needs to render first, at which point supabase-js picks up the URL query fragments an creates the auth cookie. Once that has happened (which currently needs to happen client-side) the session will be accessible server-side via the auth cookie.

You can find more details on this here: https://supabase.com/docs/guides/auth/server-side-rendering#understanding-the-authentication-flow

@dijonmusters
Copy link
Contributor

Docs: supabase/supabase#10396

@jensen
Copy link

jensen commented Nov 18, 2022

Do you still get a null session when navigating to your API route: /api/hello?

Yes - it happens on the first load after an auth. The session doesn't get hydrated until the client interacts with it once - then a reload will show you it in the middleware. But the first middleware hit after authentication always returns a null session for me.

Not sure if this helps, but the way that I handle this is with an /authenticated route. https://github.com/jensen/supabase-nextjs/blob/main/app/(new)/authenticated/page.tsx

All it does is wait patiently for that session to be set. https://github.com/jensen/supabase-nextjs/blob/main/components/auth/check.tsx

I can tell the redirect to go to the /authenticated route using the supabase api. https://github.com/jensen/supabase-nextjs/blob/9cec28b5c48c16be7fd8487c586eadc90bfef401/components/auth/buttons/discord.tsx#L15

@tiniscule
Copy link

tiniscule commented Nov 18, 2022

Unfortunately that's to be expected for Oauth (redirect) based methods.

I think a recommendation on how to handle this officially would make sense then - as it basically means auth will feel broken using the default implementation example. I wonder if there's another way to recognize the first hit after auth to let it through to the original return path

@jensen
Copy link

jensen commented Nov 18, 2022

Unfortunately that's to be expected for Oauth (redirect) based methods.

I think a recommendation on how to handle this officially would make sense then - as it basically means auth will feel broken using the default implementation example. I wonder if there's another way to recognize the first hit after auth to let it through to the original return path

I've spent dozens of hours working with Supabase auth, and I can't get around this with the query fragment based access token (when we are dealing with email confirmation or provide redirects). The only alternative I have tried was to alter GoTrue to send the tokens to my server route instead of redirecting to the client, but that doesn't work when you want Supabase to manage your instance. This is why I have had to do what I've shown above.

When working with Remix, I went so far as to handle extracting the session from the URL directly without loading React etc.

https://github.com/jensen/supabooked/blob/main/src/routes/authenticated.tsx

@rodzerjohn
Copy link

rodzerjohn commented Nov 21, 2022

How do i call inner nextjs api with session? session object in middleware returns null.

Very strange. This should only happen if you are not signed in.

If you make your middleware something like this:

import { createMiddlewareSupabaseClient } from "@supabase/auth-helpers-nextjs";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export async function middleware(req: NextRequest) {
  const res = NextResponse.next();
  const supabase = createMiddlewareSupabaseClient({ req, res });

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

  console.log({ session });

  return res;
}

export const config = {
  matcher: ["/api/hello"],
};

And call one of the Supabase auth methods client-side, for example:

await supabase.auth.signInWithPassword({
      email: 'jon@supabase.com',
      password: 'password'
    });

Do you still get a null session when navigating to your API route: /api/hello?

I forgot to mention that I'm trying to access /api/hello from server side component using fetch API. In this case session returns null in middleware. How do i work with auth in this case?

Simply navigating to /api/hello works fine.

@thorwebdev
Copy link
Member Author

Docs have been added: https://supabase.com/docs/guides/auth/auth-helpers/nextjs-server-components

Closing this issue out.

@rodzerjohn can you please open a GitHub Discussion with a link to your code and we can help over there. thanks.

@NixBiks
Copy link
Contributor

NixBiks commented Nov 22, 2022

Wonderful docs @thorwebdev - especially going beyond and adding the data fetch + real-time updates pattern!

@noskovvkirill
Copy link

noskovvkirill commented Nov 30, 2022

Having a trouble implementing it without redirect — i.e with phone based sign up. Page needs to be hard refreshed before user is authenticated. I set cookies manually and check them in middleware — it works for the server, but Supabase server client doesn't seem to catch it.

Middleware:

const supabase = createMiddlewareSupabaseClient({ req, res });

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

let currentSession = session;

const refreshToken = req.cookies.get('my-refresh-token')?.value
const accessToken = req.cookies.get('my-access-token')?.value

if (refreshToken && accessToken && refreshToken !== "" && accessToken !== "") {
  const { data, error } = await supabase.auth.setSession({ access_token: accessToken, refresh_token: refreshToken })
  if (error) {
    res.cookies.delete('my-refresh-token')
    res.cookies.delete('my-access-token')
  } else {
    if (data.session?.user && data.session?.access_token && data.session?.refresh_token) {
      currentSession = data.session
    }
  }
}

And the page with the workaround:

 const supabase = createServerComponentSupabaseClient<Database>({
    headers,
    cookies
  });
  let { data: { user } } = await supabase.auth.getUser();
  let { data: { session } } = await supabase.auth.getSession();
  
  // this works
  if (!user) {
    const cookies_ = cookies();
    const refreshToken = cookies_.get('my-refresh-token')?.value
    const accessToken = cookies_.get('my-access-token')?.value
    if (refreshToken && accessToken) {
      const { data, error } = await supabase.auth.setSession({ access_token: accessToken, refresh_token: refreshToken })
      if (error) {
        redirect('/')
      } else {
        session = data.session
        let { data: { user: user_ } } = await supabase.auth.getUser();
        user = user_
      }
    } else {
      redirect('/')
    }
  }

I'm not sure what I'm doing wrong, but the session set in the middleware isn't visible on the client if the page isn't refreshed. I have to duplicate the logic in the component.

@thorwebdev maybe you have some ideas? 🥹

@hjaber
Copy link

hjaber commented Nov 30, 2022

Having a trouble implementing it without redirect — i.e with phone based sign up. Page needs to be hard refreshed before user is authenticated. I set cookies manually and check them in middleware — it works for the server, but Supabase server client doesn't seem to catch it.

Middleware:

const supabase = createMiddlewareSupabaseClient({ req, res });

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

let currentSession = session;

const refreshToken = req.cookies.get('my-refresh-token')?.value
const accessToken = req.cookies.get('my-access-token')?.value

if (refreshToken && accessToken && refreshToken !== "" && accessToken !== "") {
  const { data, error } = await supabase.auth.setSession({ access_token: accessToken, refresh_token: refreshToken })
  if (error) {
    res.cookies.delete('my-refresh-token')
    res.cookies.delete('my-access-token')
  } else {
    if (data.session?.user && data.session?.access_token && data.session?.refresh_token) {
      currentSession = data.session
    }
  }
}

And the page with the workaround:

 const supabase = createServerComponentSupabaseClient<Database>({
    headers,
    cookies
  });
  let { data: { user } } = await supabase.auth.getUser();
  let { data: { session } } = await supabase.auth.getSession();
  
  // this works
  if (!user) {
    const cookies_ = cookies();
    const refreshToken = cookies_.get('my-refresh-token')?.value
    const accessToken = cookies_.get('my-access-token')?.value
    if (refreshToken && accessToken) {
      const { data, error } = await supabase.auth.setSession({ access_token: accessToken, refresh_token: refreshToken })
      if (error) {
        redirect('/')
      } else {
        session = data.session
        let { data: { user: user_ } } = await supabase.auth.getUser();
        user = user_
      }
    } else {
      redirect('/')
    }
  }

Not sure what I'm doing wrong, but session that was set in the middleware isn't visible on the client if the page wasn't refreshed. I have to duplicate the logic in the component.

@thorwebdev maybe you some ideas? 🥹

I have a similar issue relating to middleware rather than the server.

When following the official docs for auth helpers on server components/app directory, the supabase server client immediately updates based on the session cookies and functions appropriately.

But the middleware is out of sync with auth and requires the page to be refreshed before correctly identifying the current session.

As the cookies are correctly being updated, I believe this to be a nextjs issue and not a supabase issue.

@tiniscule
Copy link

tiniscule commented Nov 30, 2022

It's not really an "issue" on either side unfortunately - a design decision on the supabase side makes it impossible to detect the authenticated state prior to the client side page load. The server never sees the hash params, so it has no ability to handle the authentication. I took a stab at explaining and showing a workaround here: https://usebasejump.com/blog/supabase-oauth-with-nextjs-middleware

@noskovvkirill
Copy link

@tiniscule interesting, now I understand the issue better. Thanks for sharing!

@hjaber
Copy link

hjaber commented Nov 30, 2022

It's not really an "issue" on either side unfortunately - a design decision on the supabase side makes it impossible to detect the authenticated state prior to the client side page load. The server never sees the hash params, so it has no ability to handle the authentication. I took a stab at explaining and showing a workaround here: https://usebasejump.com/blog/supabase-oauth-with-nextjs-middleware

I thought this was only true for Oauth (redirect) based methods?

I'm using signInWithPassword() and still unable to utilize middleware without a refresh while using the auth helpers server components/app implementation.

@goranefbl
Copy link

It's not really an "issue" on either side unfortunately - a design decision on the supabase side makes it impossible to detect the authenticated state prior to the client side page load. The server never sees the hash params, so it has no ability to handle the authentication. I took a stab at explaining and showing a workaround here: https://usebasejump.com/blog/supabase-oauth-with-nextjs-middleware

Thanks. This works for SignIn, but not for SignUp if there is no email verification. Any solution for that?

@csalmeida
Copy link

Do you still get a null session when navigating to your API route: /api/hello?

Yes - it happens on the first load after an auth. The session doesn't get hydrated until the client interacts with it once - then a reload will show you it in the middleware. But the first middleware hit after authentication always returns a null session for me.

Not sure if this helps, but the way that I handle this is with an /authenticated route. https://github.com/jensen/supabase-nextjs/blob/main/app/(new)/authenticated/page.tsx

All it does is wait patiently for that session to be set. https://github.com/jensen/supabase-nextjs/blob/main/components/auth/check.tsx

I can tell the redirect to go to the /authenticated route using the supabase api. https://github.com/jensen/supabase-nextjs/blob/9cec28b5c48c16be7fd8487c586eadc90bfef401/components/auth/buttons/discord.tsx#L15

I have followed @jensen's approach but using createBrowserClient instead which is taken from the NextJS Server Components example but unfortunately, the session still returns null using OAuth.

Still investigating why this might be but thought I would leave my experience here in case anyone else is trying the same approach.

@trulysinclair
Copy link

@csalmeida for me I only had issues accidentally using both the supabasejs package and the auth helpers at the same time without noticing. Removing the main supabase package and making sure to only use auth helpers, everything worked perfectly fine.

@tonyxiao
Copy link

I'm getting this error when using the createServerComponentSupabaseClient.
Error: The "host" request header is not available

Did anyone else run into this and have a workaround?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request nextjs Next.js specific functionality
Projects
None yet