How To Use Supabase Auth For A Login Screen (step-by-step)

No Comments
Modified: 14.01.2023

Need to add a login system to your application but not sure where to start? Supabase auth makes it easy! In this tutorial, we’ll guide you through the process of integrating Supabase’s email/password authentication provider into your project. Follow along and see how straightforward it is to add Supabase auth to your project.

Here is the link to the GitHub project. If you have any questions along the way, feel free to ask me via chat, ask a comment or send me a mail via mail@programonaut.com.

Project structure

The goal of this project is to create a login and sign-up screen that can be used in your web application. In the next guide, we will then extend the login system and screen with the functionality for a chat app (here).

For this project, we will use Supabase for the authentication and Svelte for the front end. The process and interaction should be the same for whatever framework you use.

The final project of this post will consist of a login/register screen that allows you to create a new user and then log in with that user and another screen that allows you to sign out again.

Use supabase auth for a login screen

The project consists of three main stages. The first one is the setup of supabase. The second one is the setup of svelte and tailwind for the front end, and the last step is the creation of the screens and the connection with supabase.

Need help or want to share feedback? Join my discord community!

Supabase setup

  1. Login to supabase.com
  2. Click on new project and fill in the following information:
    • Name: Supachat
    • Database Password 
    • Region
    • Pricing: Free Plan
  3. Copy the anon public key and the project url (store them somewhere safe we will need them soon)
  4. Go to “Table Editor” and create a new table called profiles where the column id is a foreign key on auth.users:
    use supabase auth for a login screen: profiles table
    This is needed because the auth schema is not publicly available if we want to access user information. Therefore we create the data we want inside the profiles table.
  5. Go to “SQL Editor” and run the following query. The query creates a trigger that automatically generates a profiles entry when a new user is created (the trigger is inspired by this post here):
drop function if exists handle_new_user cascade;

create function public.handle_new_user()
returns trigger
language plpgsql
security definer set search_path = public
as $$
begin
  insert into public.profiles (id, email)
  values (new.id, new.email);
  return new;
end;
$$;

-- trigger the function every time a user is created
create trigger on_auth_user_created
  after insert on auth.users
  for each row execute procedure public.handle_new_user();

Project setup

KOFI Logo

If this guide is helpful to you and you like what I do, please support me with a coffee!

  1. npm create vite@latest supachat
    • Select Svelte and then Typescript
  2. cd supachat && npm i
  3. npm install @supabase/supabase-js
  4. npm install -D tailwindcss postcss autoprefixer @tailwindcss/forms
  5. npx tailwindcss init -p
  6. Add the following to tailwind.config.cjs:
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./src/**/*.{html,js,svelte,ts}'],
  theme: {
    extend: {}
  },
  plugins: [require('@tailwindcss/forms')]
};
  1. Add the following to src/app.css
@tailwind base;
@tailwind components;
@tailwind utilities;
  1. Clear the boilerplate code and files, such as the content of src/App.svelte
  2. Create .env file with the anon public key and the project URL that you copied before:
VITE_SUPABASE_URL=YOUR_SUPABASE_URL
VITE_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
  1. Now we have to create the supabase client file as src/lib/supabase.ts it contains the connection to the supabase project and a svelte store containing the currently logged-in user. To get the user, we use a supabase function:
import { createClient } from '@supabase/supabase-js'
import { writable } from 'svelte/store'

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY

export const supabase = createClient(supabaseUrl, supabaseAnonKey)

export const currentUser = writable((await supabase.auth.getUser()).data.user)

supabase.auth.onAuthStateChange(async () => {
        currentUser.set((await supabase.auth.getUser()).data.user)
    }
)

Create a login screen and connect it with Supabase auth

Now that we have the basic setup done, we will create the login screen based on this template by tailwind UI.

  1. Edit index.html and add the following classes to the html and body tags:
    <html class="h-full bg-gray-50">
    <body class="h-full">
  2. Create the file src/LoginScreen.svelte. After adding the following snippet to the file, we have the whole UI and all the necessary variables.
<script lang="ts">
  import { supabase } from "./lib/supabase";

  let email: string;
  let password: string;
  let loading: boolean = false;
  let register: boolean = false;
  let message: string = "";

  const handleLogin = async () => {};
</script>

<div class="flex min-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
  <div class="w-full max-w-md space-y-8">
    <div>
      <img class="mx-auto h-12 w-auto" src="https://tailwindui.com/img/logos/mark.svg?color=indigo&shade=600" alt="Your Company" />
      <h2 class="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">Sign in to your account</h2>
    </div>
    <!-- Added form action -->
    <form class="mt-8 space-y-6" on:submit|preventDefault={handleLogin}>
      <input type="hidden" name="remember" value="true" />
      <div class="-space-y-px rounded-md shadow-sm">
        <div>
          <label for="email-address" class="sr-only">Email address</label>
          <input
            id="email-address"
            name="email"
            type="email"
            autocomplete="email"
            required
            class="relative block w-full appearance-none rounded-none rounded-t-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
            placeholder="Email address"
            bind:value={email}
          />
        </div>
        <div>
          <label for="password" class="sr-only">Password</label>
          <input
            id="password"
            name="password"
            type="password"
            autocomplete="current-password"
            required
            class="relative block w-full appearance-none rounded-none rounded-b-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm"
            placeholder="Password"
            bind:value={password}
          />
        </div>
      </div>

      {#if message}
        <div class="flex items-center justify-between">
          <p class="text-indigo-500">{message}</p>
        </div>
      {/if}

      <!-- <div class="flex items-center justify-between">
          <div class="flex items-center">
            <input id="remember-me" name="remember-me" type="checkbox" class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
            <label for="remember-me" class="ml-2 block text-sm text-gray-900">Remember me</label>
          </div>
        </div> -->

      <div>
        <!-- Add loading indication -->
        <button
          type="submit"
          class="group relative flex w-full justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
          disabled={loading}
        >
          <span class="absolute inset-y-0 left-0 flex items-center pl-3">
            {#if !loading}
              <!-- Heroicon name: mini/lock-closed -->
              <svg
                class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400"
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 20 20"
                fill="currentColor"
                aria-hidden="true"
              >
                <path
                  fill-rule="evenodd"
                  d="M10 1a4.5 4.5 0 00-4.5 4.5V9H5a2 2 0 00-2 2v6a2 2 0 002 2h10a2 2 0 002-2v-6a2 2 0 00-2-2h-.5V5.5A4.5 4.5 0 0010 1zm3 8V5.5a3 3 0 10-6 0V9h6z"
                  clip-rule="evenodd"
                />
              </svg>
            {:else}
              <!-- Heroicon name: arrow-path -->
              <svg
                class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400 animate-spin"
                xmlns="http://www.w3.org/2000/svg"
                fill="none"
                viewBox="0 0 24 24"
                stroke-width="1.5"
                stroke="currentColor"
              >
                <path
                  stroke-linecap="round"
                  stroke-linejoin="round"
                  d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
                />
              </svg>
            {/if}
          </span>
          {loading ? "Signing in...." : "Sign in"}
        </button>

        <button
          type="submit"
          class="group relative flex w-full justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 mt-2"
          disabled={loading}
          on:click={() => (register = true)}
        >
          <span class="absolute inset-y-0 left-0 flex items-center pl-3">
            {#if loading && register}
              <!-- Heroicon name: arrow-path -->
              <svg
                class="h-5 w-5 text-indigo-500 group-hover:text-indigo-400 animate-spin"
                xmlns="http://www.w3.org/2000/svg"
                fill="none"
                viewBox="0 0 24 24"
                stroke-width="1.5"
                stroke="currentColor"
              >
                <path
                  stroke-linecap="round"
                  stroke-linejoin="round"
                  d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
                />
              </svg>
            {/if}
          </span>
          {loading && register ? "Registering...." : "Register"}
        </button>
      </div>
    </form>
  </div>
</div>
  1. Next up, we will create the functionality to register a new user in supabase. One important bit for this is in line 112 in the above snippet. There you see that when we click on the register button, we execute the function () => register = true. Through setting the variable, we know whether to register or log in a user.
    To register a user, we have to call a function provided by the supabase lib, and we have to give it the data from the two input fields like this (add this to the function handleLogin):
loading = true;

if (register) {
    const { data, error } = await supabase.auth.signUp({
        email,
        password,
    });

    if (error) {
        message = "Incorrect email or password";
    } else {
        message = "Check your email for the login link";
    }

    register = false;
} 

loading = false;
  1. Now that we can register a new user, we have to add the functionality to log in as the user. For that, we use another function provided by supabase. This time we must choose the sign-in with email and password (append the code after line 16 of the previous snippet).
else {
    const { data, error } = await supabase.auth.signInWithPassword({
        email,
        password,
    });

    if (error) {
        message = "Invalid password";
    } else {
        message = "Logged in";
    }
}
  1. With that, you should already be able to register a new user and log in as a new user. The next step is to change the screen based on the login status. For that we create a new screen as src/UserScreen.svelte with the following content:
<script lang="ts">
  import { currentUser, supabase } from "./lib/supabase";

  async function handleSignOut() {}
</script>

<div class="flex flex-col items-center justify-center w-full py-20">
  <p>{$currentUser.email}</p>
  <button
    class="group relative flex w-1/2 justify-center rounded-md border border-transparent bg-indigo-600 py-2 px-4 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 mt-2"
    on:click={handleSignOut}
  >
    <span class="absolute inset-y-0 left-0 flex items-center pl-3" />
    Sign Out
  </button>
</div>
  1. To add the logout functionality, we call another function provided by supabase. Add the following code to handleSignOut:
async function handleSignOut() {
  const { error } = await supabase.auth.signOut();

  if (error) {
    console.log("error", error);
  }
}
  1. Lastly, to swap between the two screens, we have to check whether a user is logged in or not conditionally. For that, we can use the svelte store that we created in the project setup section. Edit or create the file src/App.svelte with the following content:
<script lang="ts">
  import LoginScreen from "./LoginScreen.svelte";
  import { currentUser } from "./lib/supabase";
  import UserScreen from "./UserScreen.svelte";
</script>

{#if !$currentUser}
  <LoginScreen />
{:else}
  <UserScreen />
{/if}

With that, you have a login system base on supabase auth that consists of a login screen and a sign-out screen when you are logged in!

Conclusion

In summary, Supabase auth makes it pretty easy to add a login system to your application. We learned how to set up Supabase’s email/password authentication provider and implemented a login screen. If you have any questions or need further help, don’t hesitate to contact me via chat or email at mail@programonaut.com.

Don’t forget to subscribe to my monthly newsletter to stay up-to-date on all my blog posts and updates. Sign up now and never miss a post!

Discussion (0)

Add Comment

Your email address will not be published. Required fields are marked *