Hello, I'm Lucas Frazao. I've been working as a software engineer (SWE) for the past five years.
In this post, we'll build a simple authentication flow using ReactJS (v.18.3) and Supabase. Building authentication flows is a common task in personal projects. So, I'd like to show you how to achieve this using supabase authentication.
What is supabase?
Supabase is an open source Firebase alternative that uses PostgreSQL. This means it can be used for various purposes beyond authentication, such as storing and organizing images and data, building real-time chat applications, managing user authentication, and more. You can find more information in the official documentation: https://supabase.com/docs
Let's start
To keep things simple, we will be using vitejs for our development enviroment, npm as the package manager, and typescript because... well, I like TS!
Disclaimer: This tutorial can be followed using either TypeScript or JavaScript.
So, first of all, we need a web application to connect with supabase, so let's do this.
Run command:
npm create vite@latest
The project was named as ''auth-supabase'' but you can choose another name.
Let's give a name to project:
npm create vite@latest
? Project name: › auth-supabase
Select react on framework options:
✔ Project name: … auth-supabase
? Select a framework: › - Use arrow-keys. Return to submit.
Vanilla
Vue
❯ React
Preact
Lit
Svelte
Solid
Qwik
Others
Select TypeScript (or JavaScript) from the variant options.
✔ Project name: … auth-supabase
✔ Select a framework: › React
? Select a variant: › - Use arrow-keys. Return to submit.
❯ TypeScript
TypeScript + SWC
JavaScript
JavaScript + SWC
Remix ↗
Let's navigate to the project folder and clean things up a bit.
Run the following command:
cd auth-supabase && code .
You should see something like this:
Now, to streamline the project structure, we'll perform some cleanup steps. Here's what to do:
-> Delete the src/assets
folder.
-> Clear the content of index.css
and paste the following code:
*,
:after,
:before {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
scroll-behavior: smooth;
text-rendering: optimizeSpeed;
font-synthesis: none;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a, button {
color: inherit;
cursor: pointer;
}
-> Clear the contents of App.tsx
then past the following code:
import { FormEvent } from "react";
import "./App.css";
export default function App() {
function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const data = new FormData(event.currentTarget);
const email = data.get("email")?.toString();
const password = data.get("password")?.toString();
console.log({ email, password });
}
return (
<main>
<h1>Sign in with supabase</h1>
<form onSubmit={handleSubmit}>
<label htmlFor="email">E-mail</label>
<input
type="text"
name="email"
placeholder="email@email.com"
required
/>
<label htmlFor="password">Password</label>
<input
type="password"
name="password"
placeholder="********"
required
/>
<button type="submit">Login</button>
</form>
</main>
);
}
It's necessary install the dependencies running:
npm install
Great! If you didn't encounter any errors during the previous steps, we can move on to configuring supabase in our project. While this tutorial will focus on a traditional username and password login, supabase also supports social logins. If you'd like to explore that option, you can refer to the official documentation here:
https://supabase.com/docs/guides/auth/social-login
To install supabase, run command:
npm install @supabase/supabase-js
We can verify if our project is set up correctly by checking package.json
Once we've completed these steps, run the following command to start the development server:
npm run dev
Then, you can access your application in the browser by visiting http://localhost:5173/ . You should see something like this:
Let's configure our auth. Remember, it's necessary to create an account on supabase to follow up this post.
-> Click "Create organization" after filling out the form.
-> To create a new project, fill out all the fields in this form. For the database password, you can click "Generate a Password" and then "Copy" to securely store the generated password. This password is critical for managing your project, so be sure to keep it safe.
You don't need to change the security options for now. Just click "Create new project" to proceed.
Now, the project is setting up.
-> In the sidebar menu, locate the "Authentication" option and click on it.
-> From the user management options, select "Create New User" to add a new user.
Disclaimer: While this tutorial demonstrates creating a new user directly, it's generally recommended to prioritize security in real-world applications. For increased security, consider using the "send invitation" option, which allows users to self-register and verify their email addresses before gaining access. Additionally, explore alternative authentication methods like social logins or passwordless authentication to best suit your specific security needs.
-> Enter your email address and password. Then, select "Create user" and make sure to check "Auto Confirm User?" .
So, now we have an user to test the connection between supabase and our application. Let's navigate back to the home page and select the "Connect" option.
-> Click on "Connect", on right side.
-> Copy all content in this code block
-> In our application, create a .env file, then paste the code on it:
In Vite.js, environment variables are prefixed with
VITE_
by default. You can find more details about environment variables and modes in the official Vite documentation:
https://vitejs.dev/guide/env-and-mode
Now, let's create a utility function to connect with Supabase. Here's what to do:
-
Create a new folder: Inside the
src
directory, create a new folder namedutils
. -
Create a TypeScript file: Inside the newly created
utils
folder, create a new file namedsupabase.ts
. -
Add the following code: Paste the code snippet you want to add to
supabase.ts
below.
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
export const supabase = createClient(supabaseUrl, supabaseKey);
Now that we've finished configuring everything, let's move on to final stage.
Final stage
We already have the email and password values stored in variables, and they're currently being used for logging and potentially within the handleSubmit()
, but let's make some changes. Let's refactor this function to be asynchronous and incorporate a try-catch block.
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const data = new FormData(event.currentTarget);
const email = data.get("email")?.toString();
const password = data.get("password")?.toString();
try {
console.log({ email, password });
} catch (error) {
console.error(error);
}
}
Benefits of using try-catch with form submissions and API calls:
- Prevents unexpected crashes: By catching potential errors, you can gracefully handle them and avoid disrupting the user experience.
- Custom error handling: The try-catch block allows you to provide informative error messages to the user in case of issues during form submission or API interaction.
Great improvement! Building on error handling, let's add some basic validation before the try-catch block to prevent undefined values from causing errors.
if (!email || !password) return;
Now, let's import supabase
function from our supabase.ts
into the src/App.tsx
file. Then, we can utilize it within the try-catch block of the handleSubmit
function.
import { supabase } from "./utils/supabase";
/* code ... **/
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const data = new FormData(event.currentTarget);
const email = data.get("email")?.toString();
const password = data.get("password")?.toString();
if (!email || !password) return;
try {
const {} = await supabase.auth.signInWithPassword({ email, password });
} catch (error) {
console.error(error);
}
}
It's possible to use two properties from signInWithPassword()
, error and data, we'll use both, data to set token on application and error to return an error to user. Look, we changed the name of data to response, because we already use a variable called data.
const { error, data: response } = await supabase.auth.signInWithPassword({
email,
password,
});
Now that we've incorporated a try-catch block and have access to the error
object, let's add a conditional statement. This statement will determine whether to throw the error or set the token in local storage based on the outcome of the signInWithPassword
function.
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const data = new FormData(event.currentTarget);
const email = data.get("email")?.toString();
const password = data.get("password")?.toString();
if (!email || !password) return;
try {
const { error, data: response } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
throw new Error(error.message);
} else {
const accessToken = response.session?.access_token;
localStorage.setItem("access_token", accessToken);
}
} catch (error) {
console.error("occured an error:", error);
}
}
To verify a successful login, we can check your browser's local storage. Here's how:
-
Open the developer tools:
-
Windows: Press
Ctrl + Shift + J
simultaneously. -
macOS: Press
Command (⌘) + Option (⌥) + J
simultaneously.
-
Windows: Press
- Navigate to the "Application" tab.
- In the left sidebar, locate the "Local Storage" section.
- Look for a key named "access_token". The presence of this key and its corresponding value indicate a successful login. You should see something like this:
Bonus
Now that we've established the sign-in workflow, let's explore how to implement sign-out functionality.
The sign-out process shares a lot in common with handleSubmit()
. In essence, they follow a similar pattern or approach. Check the code example below for a detailed implementation.
async function handleSignOut() {
try {
const { error } = await supabase.auth.signOut();
if (error) {
throw new Error(error.message);
}
} catch (error) {
console.error(error);
}
}
It's important to built a button to call signOut
function.
<button type="button" onClick={handleSignOut}>
sign out
</button>
After clicking the sign-out button, you can verify that the sign-out process was successful by checking your browser's local storage. Since Supabase uses local storage to store authentication tokens, a successful sign-out will result in the deletion of the corresponding token object.
Remember, the key named access_token was created by us to store token. After signing out, we could to remove this key to ensure a clean and secure session termination. Here's a validate you can add within the
handleSignOut()
function to achieve this:
// Add this if you want to clear all localStorage
if (error) {
throw new Error(error.message);
} else {
localStorage.clear()
}
// Add this if you want to delete an especific key
if (error) {
throw new Error(error.message);
} else {
localStorage.removeItem('access_token')
}
Conclusion
We've built a secure user authentication flow using Supabase in our application. This guide covered essential steps like supabase initialization, sign-in, and sign-out with local storage management. Now you have a solid foundation for user authentication - feel free to customize it for your project's needs and explore more supabase functionalities!
Top comments (0)