8 min read

Tutorial: Build a simple auth app with Supabase

💡
This is a tutorial that solves the "Build a simple auth app with Supabase" exercise.
If you haven't already, try to solve it yourself first before reading the solution!

Creating the backend app

To get started, we should first create a "backend" app, so we can use it for authenticating our users.

You can create a new project from your Supabase dashboard.
It's ok to just use the default settings:

Next, you'll see a page with API details. Make a note of the API URL and the anon public  key. We will use to connect to the backend from the React app.

Creating the React app

To create the frontend, we can just use the Vite starter:

npm create vite@latest

Then, we can install Tailwind CSS for styling.

With the scaffolding in place, we can go ahead and install the Supabase JS library to easily access the backend:

npm install @supabase/supabase-js

We can initialise it with our API URL and anon key:

// src/supabase.js
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  "https://YOUR_URL.supabase.co",
  "YOUR_ANON_KEY"
);

export default supabase;

Creating the user registration

You wouldn't normally start with creating users, but since we have none, it helps to create the "Signup" page first.

Supabase API expects us to pass a username and password when creating users:

const { data, error } = await supabase.auth.signUp({
  email: 'example@email.com',
  password: 'example-password',
})

They also mention in the docs that, by default, users need to confirm their email before the account is activated. Since this would only add more complexity to this tutorial, let's go ahead and disable this option in the project auth settings (don't forget to click Save!):

Next, let's create a "Signup" component that shows an email and password and calls the method above on submit.

To keep things simple, I followed the styles used by Tailwind CSS in their auth form examples:

This is the code for the form:

import supabase from "../supabase";

const SignUp = () => {
  const handleSubmit = async (e) => {
    ...
  };

  return (
    <div className="w-96 m-auto">
      <h2 className="m-10 text-center text-2xl font-bold">
        Create a new account
      </h2>
      {error && (
        <div className=" text-red-700 p-4 text-center" role="alert">
          Error - {error}
        </div>
      )}
      <form onSubmit={handleSubmit}>
        <div>
          <label>
            Email address
            <input
              name="email"
              type="email"
              autoComplete="email"
              required
              className="w-full p-1.5 shadow-sm ring-1 ring-inset ring-gray-300 mb-2"
            />
          </label>
        </div>

        <div>
          <label>
            Password
            <input
              name="password"
              type="password"
              autoComplete="new-password"
              required
              className="w-full rounded-md p-1.5 shadow-sm ring-1 ring-inset ring-gray-300 mb-2"
            />
          </label>
        </div>

        <div>
          <button
            type="submit"
			className="w-full rounded-md bg-indigo-600 px-3 py-1.5 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline">
            Sign up
          </button>
        </div>
      </form>
    </div>
  );
};

export default SignUp;

On success, we would normally redirect the user, but since we don't have routing setup, let's just show a message.

import { useState } from "react";
import supabase from "../supabase";

const SignUp = () => {
  const [isRegistered, setIsRegistered] = useState(false);
  const [error, setError] = useState(null);
  const handleSubmit = async (e) => {
    e.preventDefault();

    const formData = new FormData(e.target);
    const { data, error } = await supabase.auth.signUp({
      email: formData.get("email"),
      password: formData.get("password"),
    });
    console.log(data, error);
    if (!error) {
      setIsRegistered(true);
    } else {
      setError(error.message);
    }
  };

  if (isRegistered) {
    return (
      <div className="m-10 text-center font-bold">
        You have successfully registered! 
      </div>
    );
  }

  // otherwise return the sign up form
  return ( ...);
};

export default SignUp;

By default, Supabase has a 60 second limit between signup requests, so soon you'll get the following error - great opportunity to check our error handling works as expected!

Next, let's add routing so we can have separate pages for login, signup and the dashboard.

Adding routing

First, let's install React Router:

npm install react-router-dom

We can follow their official tutorial for setting things up.
We just need to add the router to App.jsx :

import { createBrowserRouter, RouterProvider } from "react-router-dom";
import SignUp from "./components/SignUp";

const router = createBrowserRouter([
  {
    path: "/",
    element: <div>Hello world!</div>,
  },
  {
    path: "/signup",
    element: <SignUp />,
  },
]);

export default function App() {
  return (
    <div>
      <RouterProvider router={router} />
    </div>
  );
}

And that's it! Navigating to /signup shows the sign up page, as expected.

Adding the login page

Let's create a new component for the user to login.

We need to make a choice - do we reuse the same form we used for signing up, since they are very similar? Or do we make a new form, specifically for login?

For the purpose of this tutorial, I suggest we just create a new form. This allows us to keep the components separate and tweak them as needed.

Notice we added a link pointing the user to register an account in case he doesn't already have one using React Router's Link component.

import { useState } from "react";
import supabase from "../supabase";
import { useNavigate } from "react-router-dom";
import { Link } from "react-router-dom";

const Login = () => {
  const [error, setError] = useState(null);
  ...

  const handleSubmit = async (e) => {
    ...
  };

  return (
    <div className="w-96 m-auto">
      <h2 className="m-10 text-center text-2xl font-bold">Sign in</h2>
      {error && (
        <div className=" text-red-700 p-4 text-center" role="alert">
          Error - {error}
        </div>
      )}
      <form onSubmit={handleSubmit}>
        <div>
          <label>
            Email address
            <input
              name="email"
              type="email"
              autoComplete="email"
              required
              className="w-full p-1.5 shadow-sm ring-1 ring-inset ring-gray-300 mb-2"
            />
          </label>
        </div>

        <div>
          <label>
            Password
            <input
              name="password"
              type="password"
              autoComplete="new-password"
              required
              className="w-full rounded-md p-1.5 shadow-sm ring-1 ring-inset ring-gray-300 mb-2"
            />
          </label>
        </div>

        <div>
          <button
            type="submit"
            className="w-full rounded-md bg-indigo-600 px-3 py-1.5  text-white shadow-sm hover:bg-indigo-500 focus-visible:outline"
          >
            Login
          </button>
        </div>
      </form>
      <div className="mt-4">
        Don&apos;t have an account yet?{" "}
        <Link to={"/signup"} className="text-indigo-600 hover:text-indigo-500">
          Register a new account
        </Link>
      </div>
    </div>
  );
};

export default Login;

Now, this looks nice, but it doesn't actually login the user :) So let's add that as well.

To start, it helps to confirm what users we already have registered in our app.
Login to your Supabase Dashboard, navigate to your project and go to the "SQL Editor". If you type select * from auth.users and hit "Run", you should at least one user - the ones we registered earlier:

To sign in a user, the docs give us the following example:

const { data, error } = await supabase.auth.signInWithPassword({
  email: 'example@email.com',
  password: 'example-password',
})

Let's update the login form to sign in the user and then redirect to the homepage:

import { useState } from "react";
import supabase from "../supabase";
import { useNavigate } from "react-router-dom";

const Login = () => {
  const [error, setError] = useState(null);
  const navigate = useNavigate();

  const handleSubmit = async (e) => {
    e.preventDefault();

    const formData = new FormData(e.target);
    const { data, error } = await supabase.auth.signInWithPassword({
      email: formData.get("email"),
      password: formData.get("password"),
    });
    console.log(data, error);
    if (error) {
      setError(error.message);
      return;
    }

    // Redirect to Dahsboard
    return navigate(`/`);
  };

  return (...);
};

export default Login;

Notice we're using the useNavigate hook from React Router to redirect the user to the Dashboard.

This works, but the Dashboard is just a dummy "Hello world", so let's create a new Dashboard component. If the user is logged in, we can show his info and otherwise we can redirect back to the login page.

Creating the Dashboard component

For the Dashboard, let's first show the user that he is logged in and allow him to log out.

To check if a user is logged in, we can use the getSession API method:

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

The component will be rather straightforward:

import { useState, useEffect } from "react";
import supabase from "../supabase";

const Dashboard = () => {
  const [user, setUser] = useState(null);
  useEffect(() => {
    const getLoggedInUser = async () => {
      const { data } = await supabase.auth.getSession();
      console.log({ data });
      setUser(data.session.user);
    };
    getLoggedInUser();
  }, []);

  return (
    <div className="w-96 m-auto text-center">
      <h2 className="m-10 text-center text-2xl font-bold">Dashboard</h2>
      <div>You are logged in as {user?.email}.</div>      
    </div>
  );
};

export default Dashboard;

Logging out the user

Next, let's add a link that allows the user to log out.

The docs tell us we can call the sign out method:

const { error } = await supabase.auth.signOut()

After signing out, we can redirect the user back to the login page:

import { useState, useEffect } from "react";
import supabase from "../supabase";
import { useNavigate } from "react-router-dom";

const Dashboard = () => {
  const [user, setUser] = useState(null);
  const [error, setError] = useState(null);
  const navigate = useNavigate();

  ...

  const handleLogout = async () => {
    const { error } = await supabase.auth.signOut();
    if (error) {
      setError(error);
      return;
    }
    navigate("/login");
  };

  return (
    <div className="w-96 m-auto text-center">
		...
      <div>
        <a
          href="#"
          className="text-indigo-600 hover:text-indigo-500"
          onClick={handleLogout}
        >
          Log out
        </a>
      </div>
    </div>
  );
};

Protecting the Dashboard

With this we have the whole flow in place: users can sign up, login in, log out and browse the dashboard. But there's a catch - what if a user who is not logged in navigates directly to the dashboard? We should check if an unauthorised user tries to access the page and redirect to the login screen.

To check if the user is not logged in, we can check if the session property of the supabase.auth.getSession() api call is null:

useEffect(() => {
    const getLoggedInUser = async () => {
      const { data, error } = await supabase.auth.getSession();
      if (error) {
        setError(error);
        setUser(null);
        return;
      }
      if (!data.session) {
        // means user is not logged in
        // so redirect to login page
        navigate("/login");
        return;
      } else {
        setUser(data.session.user);
      }
    };
    getLoggedInUser();
  }, [navigate]);

And that's it! Users are now redirected to the "Login" screen if they try to access the Dashboard without being logged in.

With this last step in place, we have completed the flow described in the initial requirements:

If user is logged in:

  • show dashboard available at /

Otherwise, redirect to /login:

  • shows username and password
  • links to /signup in case the user doesn't have an account yet

You can check out the full code for this solution on Github: https://github.com/reactpractice-dev/supabase-dashboard-with-auth.

If you enjoyed this challenge and would like more practice exercises with routing and auth, let me know in the comments below!