Skip to content

clxrityy/nextjs-supabase

Repository files navigation

Nextjs Supabase Auth & Realtime Template

demo

Features

Set up

  1. Clone the repository (or click use template)
git clone https://github.com/clxrityy/nextjs-supabase-auth.git
  1. Include your .env variables
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
NEXT_PUBLIC_SERVICE_ROLE_KEY=

More information about quickstarting Supabse with Nextjs: Using Supabase with Nextjs


Middleware

The middleware will redirect any unauthenticated user to the /login page.

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

export default async function middleware(req: NextRequest) {
  // Response object
  const res = NextResponse.next();

  // Initialize supabase middleware client
  const supabase = createMiddlewareClient({ req, res });

  // Deconstruct the session object from supabase auth
  const {
    data: { session },
  } = await supabase.auth.getSession();

  // If no session is present, rewrite the route to /login
  if (!session) {
    return NextResponse.rewrite(new URL("/login"), req.url);
  }

  // Otherwise, return the response
  return res;
}

export const config = {
  /**
   * @see https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
   */
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

More information about Nextjs middleware: Routing: Middleware


Authentication

Once the user is redirected to the /login route, they're prompted with a form to either login or sign up. There are no external authentication providers (such as Google, Discord, GitHub, etc.) configured currently, but they can easily be set up.

You must login with your email & password

login page

Note: The form is currently configured so that the email & password inputs work for logging in OR signing up (rather than having a separate /sign-up route).

Logging in

  • The email & password values are saved with useState() and set with the handleChange() function.
const [data, setData] = useState<{
  email: string;
  password: string;
}>();

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  const { name, value } = e.target;

  setData((prev: any) => ({
    ...prev,
    [name]: value,
  }));
};
  • A <form /> element is rendered with a POST action to /api/auth/login.
<form action={"/api/auth/login"} method="post">
  <input type="text" name="email" onChange={handleChange} />
  <input type="password" name="password" />
  <Button onClick={login}>Login</Button>
</form>
  • Upon clicking the Login button, the login() function is called; which will use the data to sign in with supabase auth (and make the post request to the API route).
const login = async () => {
  if (data) {
    try {
      const { data: authData, error } =
        await supabaseClient.auth.signInWithPassword({
          email: data.email,
          password: data.password,
        });

      if (error) {
        console.log(`Error: ${error.message}`);
      }
    } catch (e) {
      console.log(`Error: ${e}`);
    } finally {
      redirect("/");
    }
  }
};

Signing up

As previously stated, there is no sign up route. The values within the input elements on the login page can be used to sign up as well.

  • A separate form is rendered which has a POST action to /api/auth/sign-up and the Sign Up button will run the signUp() function on click.
const signUp = async () => {
  if (data) {
    try {
      const { data: authData, error } = await supabaseClient.auth.signUp({
        email: data.email,
        password: data.password,
      });

      if (error) {
        console.log(`Error: ${error.message}`);
      }
    } catch (e) {
      console.log(`Error: ${e}`);
    }
  }
};
<form
  className="flex flex-col items-center justify-center gap-2"
  action={"/api/auth/sign-up"}
  method="post"
>
  <p className="text-sm lg:text-base font-bold text-gray-700">
    Don&#39;t have an account?
  </p>
  <Button onClick={signUp} className="font-semibold" variant={"secondary"}>
    Sign up
  </Button>
</form>

API Auth Routes

/api/auth/login

/api/auth/sign-up

  • Also grabs the form data.
  • ALSO utilizes createRouteHandlerClient()
  • Calls auth.signUp(), passing in the email, password, and email redirect callback.
// api/auth/sign-up
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/handlers";
import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
  // ...
  const cookieStore = cookies();

  const supabase = createRouteHandlerClient({
    cookies: () => cookieStore,
  });

  try {
    await supabase.auth.signUp({
      email,
      password,
      options: {
        emailRedirectTo: `${url.origin}/api/auth/callback`,
      },
    });
  } catch (e) {
    // ...
  }

  return NextResponse.redirect(url.origin, {
    status: 301,
  });
}

/api/auth/callback

  • Gets the code from the url search params and turns it into a session.
const url = new URL(req.url);

const code = url.searchParams.get("code");

if (code) {
  const cookieStore = cookies();
  const supabase = createRouteHandlerClient({
    cookies: () => cookieStore,
  });

  await supabase.auth.exchangeCodeForSession(code);
}

return NextResponse.redirect(url.origin);

Realtime Tables

In order to receive live updates, you must:

  • Create a table create a table

  • Enable realtime enable realtime

  • Create a channel to subscribe to realtime changes within a useEffect() hook.

    useEffect(() => {
      const channel = supabase.channel("whatever you wanna call it")
        .on("postgres_changes", {
          event: "INSERT", // listen to whenever a new row is added
          schema: "public",
          table: "table-name"
        }, (payload) => {
          // handle the payload
        }).subscribe(); // subscribe to the channel
    
        /**
         * finally, return a callback function to unsubscribe when the component unmounts
         */
        return () => {
          channel.unsubscribe(); 
        }
    }, [])

    Note: Make sure to mark the component with "use client"


Custom hooks

React hooks (such as useState() & useEffect()) can be utilized to create some custom hooks that will ease the usage of supabase within client components.

For instance, this live click counter

click counter

Rather than calling the supabase client directly in the component, here's how the hook is set up:

"use client"

export default function useClicks() {
  const [clicks, setClicks] = useState<{[key: number]: {
    id: number:
    created_at: string;
    userId: string;
  }}>();

  const getClicks = async () => {
    const { data, error } = await supabaseClient
      .from("clicks-table")
      .select("*");

    if (data) {
      setClicks(data);
    }

    // ...
  };

  return {
    getClicks,
    clicks,
    // ...
  }
}

Then, within a client component, deconstruct the getClicks() function from the hook, and call it to retreive all the clicks.

"use client"

export default function Component() {
  const { clicks, getClicks } = useClicks();
  // ...
}

About

Next.js & Supabase realtime updates & authentication template

Topics

Resources

Stars

Watchers

Forks