Supabase is an open source managed back-end platform. It's a direct alternative to Firebase, which is owned by Google and closed source.
Supabase comes with features such as authentication, object storage, and managed databases. Everything is built on top of open source tools, such as PostgREST and GoTrue. If you want, you can also self-host your own instance of Supabase. As of today, Supabase is in public Beta.
In this tutorial you will learn how to build an a simple React application with authentication using Create React App (CRA). Supabase will serve as the back-end for the authentication part. The application will include sign in, sign up, and a private route than can only be accessed with valid credentials.
If you wanna jump straight to the code, you can check the GitHub repository.
Setting up Supabase
VIsit Supabase's website to create a new account. Click the "Start your project" button and sign in with your GitHub account.
After signing in to the dashboard, hit the green "New Project" button. A modal like this should appear:
Choose a name for your project and a region close to you. It's required that you set a database password too, but we won't be using any on this tutorial.
It will take a few minutes for the project to be fully created. After it's done, go to Settings > API and copy the URL & Public Anonymous API key. Save the values somewhere, you will need them later on.
π‘ Tip: To make it easier to test the sign up flow, you can also disable email confirmations on Authentication > Settings > Disable Email Confirmations.
Setting up the project
Create a new project using Create React App:
npx create-react-app supabase-auth-react
I usually do some cleanup on new CRA projects before start developing. Here's how the project structure looks like after moving files around and deleting a few imports:
.
βββ package.json
βββ .env.local
βββ src
βββ components
βΒ Β βββ App.js # Moved from src/
βΒ Β βββ Dashboard.js
βΒ Β βββ Login.js
βΒ Β βββ PrivateRoute.js
βΒ Β βββ Signup.js
βββ contexts
βΒ Β βββ Auth.js
βββ index.js # Already created by CRA
βββ supabase.js
Feel free to recreate the same file structure. Don't worry about adding any code or trying to make sense of all the components just yet, we will go through everything later.
The src/index.js
and src/components/App.js
were already created by CRA. Here's how they look after cleaning up:
// src/components/App.js
export function App() {
return (
<div>
<h1>supabase-auth-react</h1>
</div>
)
}
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { App } from './components/App'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)
Setting up the Supabase Client Library
First, install the Supabase JavaScript client library on your project:
npm install @supabase/supabase-js
Now add the code to initialize Supabase on src/supabase.js
:
// src/supabase.js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.REACT_APP_SUPABASE_URL,
process.env.REACT_APP_SUPABASE_PUBLIC_KEY
)
export { supabase }
In your .env.local
file, add the URL and Public Anonymous API Key saved from the first step:
# .env.local
REACT_APP_SUPABASE_URL="https://YOUR_SUPABASE_URL.supabase.co"
REACT_APP_SUPABASE_PUBLIC_KEY="eyJKhbGciOisJIUzI1Nd2iIsInR5cCsI6..."
Create authentication pages
Let's write the code for the Signup
, Login
and Dashboard
components. These will be the three main pages of the application.
For now, let's just focus on writing a boilerplate for those components, without any authentication logic. Start by writing the Signup
component:
// src/components/Signup.js
import { useRef, useState } from 'react'
export function Signup() {
const emailRef = useRef()
const passwordRef = useRef()
async function handleSubmit(e) {
e.preventDefault()
// @TODO: add sign up logic
}
return (
<>
<form onSubmit={handleSubmit}>
<label htmlFor="input-email">Email</label>
<input id="input-email" type="email" ref={emailRef} />
<label htmlFor="input-password">Password</label>
<input id="input-password" type="password" ref={passwordRef} />
<br />
<button type="submit">Sign up</button>
</form>
</>
)
}
The Login
component looks very similar to Signup
, with a few differences:
// src/components/Login.js
import { useRef, useState } from 'react'
export function Login() {
const emailRef = useRef()
const passwordRef = useRef()
async function handleSubmit(e) {
e.preventDefault()
// @TODO: add login logic
}
return (
<>
<form onSubmit={handleSubmit}>
<label htmlFor="input-email">Email</label>
<input id="input-email" type="email" ref={emailRef} />
<label htmlFor="input-password">Password</label>
<input id="input-password" type="password" ref={passwordRef} />
<br />
<button type="submit">Login</button>
</form>
</>
)
}
The Dashboard
is a simple component that displays a greeting message and offers the user to sign out:
// src/components/Dashboard.js
export function Dashboard() {
async function handleSignOut() {
// @TODO: add sign out logic
}
return (
<div>
<p>Welcome!</p>
<button onClick={handleSignOut}>Sign out</button>
</div>
)
}
Routing components with React Router
So far the components are isolated. There is no routing between the Signup
, Login
and Dashboard
pages.
Let's work on that by adding React Router to the project:
npm install react-router-dom
In src/components/App.js
, declare a route for each of the components created before:
// src/components/App.js
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
import { Signup } from './Signup'
import { Login } from './Login'
import { Dashboard } from './Dashboard'
export function App() {
return (
<div>
<h1>supabase-auth-react</h1>
{/* Add routes hereπ */}
<Router>
<Switch>
<Route exact path="/" component={Dashboard} />
<Route path="/signup" component={Signup} />
<Route path="/login" component={Login} />
</Switch>
</Router>
</div>
)
}
Let's also add links to navigate between the Signup
and Login
components:
// src/components/Signup.js
import { Link } from 'react-router-dom'
export function Signup() {
// ...
return (
<>
<form onSubmit={handleSubmit}>{/* ... */}</form>
<br />
{/* Add this π */}
<p>
Already have an account? <Link to="/login">Log In</Link>
</p>
</>
)
}
// src/components/Login.js
import { Link } from 'react-router-dom'
export function Login() {
// ...
return (
<>
<form onSubmit={handleSubmit}>{/* ... */}</form>
<br />
{/* Add this π */}
<p>
Don't have an account? <Link to="/signup">Sign Up</Link>
</p>
</>
)
}
You can test the navigation between components by running the project and clicking on the links or changing the URL in the navigation bar:
Shameless plug: you may notice that the HTML looks a bit different, that's because I am using axist, a tiny drop-in CSS library that I built.
Adding the authentication logic
To set up the authentication logic for the app, we're going to use React's Context API.
The Context API allows sharing data to a tree of components without explicitly passing props through every level of the tree. It's used to share data that is considered "global" (within that component tree).
You can read more about React Context in the official documentation.
In this tutorial, we will use Context to share data associated with the user and the authentication operations. All this information will come from Supabase and will be needed on multiple parts of the app.
Let's start by adding code on src/contexts/Auth.js
. First, create a Context object:
// src/contexts/Auth.js
import React, { useContext, useState, useEffect } from 'react'
import { supabase } from '../supabase'
const AuthContext = React.createContext()
// ...
Now, in the same file, create a Provider component called AuthProvider
:
// src/contexts/Auth.js
// ...
export function AuthProvider({ children }) {
const [user, setUser] = useState()
const [loading, setLoading] = useState(true)
useEffect(() => {
// Check active sessions and sets the user
const session = supabase.auth.session()
setUser(session?.user ?? null)
setLoading(false)
// Listen for changes on auth state (logged in, signed out, etc.)
const { data: listener } = supabase.auth.onAuthStateChange(
async (event, session) => {
setUser(session?.user ?? null)
setLoading(false)
}
)
return () => {
listener?.unsubscribe()
}
}, [])
// Will be passed down to Signup, Login and Dashboard components
const value = {
signUp: (data) => supabase.auth.signUp(data),
signIn: (data) => supabase.auth.signIn(data),
signOut: () => supabase.auth.signOut(),
user,
}
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
)
}
// ...
The AuthProvider
does three things:
- Calls
supabase.auth.session
to find out the current state of the user and update the user object. - Listens for changes in the authentication state (user signed in, logged out, created a new account, etc.) by subscribing to
supabase.auth.onAuthStateChange
function. - Prepares the object that will be shared by its children components (
value
prop). In this case, any components down the tree will have access to thesignUp
,signIn
,signOut
functions and theuser
object. They will be used by theSignup
,Login
andDashboard
components later on.
The loading
state property will make sure the child components are not rendered before we know anything about the current authentication state of the user.
Now, create a useAuth
function to help with accessing the context inside the children components:
// src/contexts/Auth.js
// ...
export function useAuth() {
return useContext(AuthContext)
}
You can check how the src/contexts/Auth.js
looks after all the changes on the GitHub repository.
Lastly, we need to wrap the Signup
, Login
and Dashboard
components with the AuthProvider
:
// src/components/App.js
// ...
import { AuthProvider } from '../contexts/Auth'
export function App() {
return (
<div>
<h1>supabase-auth-react</h1>
<Router>
{/* Wrap routes in the AuthProvider π */}
<AuthProvider>
<Switch>
<PrivateRoute exact path="/" component={Dashboard} />
<Route path="/signup" component={Signup} />
<Route path="/login" component={Login} />
</Switch>
</AuthProvider>
</Router>
</div>
)
}
Adding authentication to the components
Remember the @TODO
s you left earlier in the components? Now it's time to, well, do them.
The functions needed by the components - signUp
, signIn
and signOut
- as well as the user
object are available through the Context. We can now get those values using the useAuth
function.
Let's start by adding the sign up logic to the Signup
component:
// src/components/Signup.js
import { useRef, useState } from 'react'
import { useHistory, Link } from 'react-router-dom'
import { useAuth } from '../contexts/Auth'
export function Signup() {
const emailRef = useRef()
const passwordRef = useRef()
// Get signUp function from the auth context
const { signUp } = useAuth()
const history = useHistory()
async function handleSubmit(e) {
e.preventDefault()
// Get email and password input values
const email = emailRef.current.value
const password = passwordRef.current.value
// Calls `signUp` function from the context
const { error } = await signUp({ email, password })
if (error) {
alert('error signing in')
} else {
// Redirect user to Dashboard
history.push('/')
}
}
return (
<>
<form onSubmit={handleSubmit}>{/* ... */}</form>
<br />
<p>
Already have an account? <Link to="/login">Log In</Link>
</p>
</>
)
}
The Login
component will look very similar. The main difference is that you will call signIn
instead of signUp
:
// src/components/Login.js
import { useRef, useState } from 'react'
import { useHistory, Link } from 'react-router-dom'
import { useAuth } from '../contexts/Auth'
export function Login() {
const emailRef = useRef()
const passwordRef = useRef()
// Get signUp function from the auth context
const { signIn } = useAuth()
const history = useHistory()
async function handleSubmit(e) {
e.preventDefault()
// Get email and password input values
const email = emailRef.current.value
const password = passwordRef.current.value
// Calls `signIn` function from the context
const { error } = await signIn({ email, password })
if (error) {
alert('error signing in')
} else {
// Redirect user to Dashboard
history.push('/')
}
}
return (
<>
<form onSubmit={handleSubmit}>{/* ... */}</form>
<br />
<p>
Don't have an account? <Link to="/signup">Sign Up</Link>
</p>
</>
)
}
Lastly, change the Dashboard
so the user can sign out of the application. You can also display some basic information together with the greeting message, such as the user ID:
// src/components/Dashboard.js
import { useHistory } from 'react-router'
import { useAuth } from '../contexts/Auth'
export function Dashboard() {
// Get current user and signOut function from context
const { user, signOut } = useAuth()
const history = useHistory()
async function handleSignOut() {
// Ends user session
await signOut()
// Redirects the user to Login page
history.push('/login')
}
return (
<div>
{/* Change it to display the user ID too π*/}
<p>Welcome, {user?.id}!</p>
<button onClick={handleSignOut}>Sign out</button>
</div>
)
}
Protecting routes
Currently, all the authentication logic is in place, but the Dashboard
component remains publicly accessible. Anyone who happens to fall on locahost:3000 would see a broken version of the dashboard.
Let's fix that by protecting the route. If a user that is not authenticated tries to access it, they will be redirected to the login page.
Start by creating a PrivateRoute
component:
// src/components/PrivateRoute.js
import React from 'react'
import { Route, Redirect } from 'react-router-dom'
import { useAuth } from '../contexts/Auth'
export function PrivateRoute({ component: Component, ...rest }) {
const { user } = useAuth()
return (
<Route
{...rest}
render={(props) => {
// Renders the page only if `user` is present (user is authenticated)
// Otherwise, redirect to the login page
return user ? <Component {...props} /> : <Redirect to="/login" />
}}
></Route>
)
}
The PrivateRoute
wraps the Route
component from React Router and passes down the props to it. It only renders the page if the user
object is not null (the user is authenticated).
If the user
object is empty, a redirect to the login page will be made by Redirect
component from React Router.
Finally, update the dashboard route in the App
component to use a PrivateRoute
instead:
// src/components/App.js
- <Route exact path="/" component={Dashboard} />
+ <PrivateRoute exact path="/" component={Dashboard} />
Done! The dashboard is only available for authenticated users.
Final result
This is how the final version of the application should look like:
You can see the sign up, login, and sign out working. The dashboard page is also protected, attempting to access it by changing the URL redirects the user to the login page. Notice the user ID showing there too.
Going further
There are a few things that we could add for a more complete authentication flow:
Password reset. I intentionally left it out for simplicity, but Supabase supports password reset through their API. It does all the heavy-lifting for you, including sending the email to the user with the reset instructions.
Authentication providers. When logging in, you can also specify different authentication providers like Google, Facebook and GitHub. Check out the docs.
User operations. You can also add metatada to the authenticated user using the update function. With this, you could build for example a basic user profile page.
Thanks for reading this far!
Top comments (0)