Use Supabase Auth with React
Learn how to use Supabase Auth with React.js.
Create a new Supabase project
Launch a new project in the Supabase Dashboard.
Your new database has a table for storing your users. You can see that this table is currently empty by running some SQL in the SQL Editor.
SQL_EDITOR
1select * from auth.users;Create a React app
Create a React app using a Vite template.
Terminal
1npm create vite@latest my-app -- --template reactInstall the Supabase client library
Navigate to the React app and install the Supabase libraries.
Terminal
1cd my-app && npm install @supabase/supabase-jsDeclare Supabase Environment Variables
Create .env.local and populate with your project's URL and Key.
Changes to API keys
Supabase is changing the way keys work to improve project security and developer experience. You can read the full announcement, but in the transition period, you can use both the current anon and service_role keys and the new publishable key with the form sb_publishable_xxx which will replace the older keys.
To get the key values, open the API Keys section of a project's Settings page and do the following:
- For legacy keys, copy the
anonkey for client-side operations and theservice_rolekey for server-side operations from the Legacy API Keys tab. - For new keys, open the API Keys tab, if you don't have a publishable key already, click Create new API Keys, and copy the value from the Publishable key section.
.env.local
12VITE_SUPABASE_URL=your-project-urlVITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=sb_publishable_... or anon keySet up your login component
Explore drop-in UI components for your Supabase app.
UI components built on shadcn/ui that connect to Supabase via a single command.
Explore ComponentsIn App.jsx, create a Supabase client using your Project URL and key.
You can configure the Auth component to display whenever there is no session inside supabase.auth.getSession()
src/App.jsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153import "./index.css";import { useState, useEffect } from "react";import { createClient } from "@supabase/supabase-js";const supabase = createClient(import.meta.env.VITE_SUPABASE_URL, import.meta.env.VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY);export default function App() { const [loading, setLoading] = useState(false); const [email, setEmail] = useState(""); const [session, setSession] = useState(null); // Check URL params on initial render const params = new URLSearchParams(window.location.search); const hasTokenHash = params.get("token_hash"); const [verifying, setVerifying] = useState(!!hasTokenHash); const [authError, setAuthError] = useState(null); const [authSuccess, setAuthSuccess] = useState(false); useEffect(() => { // Check if we have token_hash in URL (magic link callback) const params = new URLSearchParams(window.location.search); const token_hash = params.get("token_hash"); const type = params.get("type"); if (token_hash) { // Verify the OTP token supabase.auth.verifyOtp({ token_hash, type: type || "email", }).then(({ error }) => { if (error) { setAuthError(error.message); } else { setAuthSuccess(true); // Clear URL params window.history.replaceState({}, document.title, "/"); } setVerifying(false); }); } // Check for existing session supabase.auth.getSession().then(({ data: { session } }) => { setSession(session); }); // Listen for auth changes const { data: { subscription }, } = supabase.auth.onAuthStateChange((_event, session) => { setSession(session); }); return () => subscription.unsubscribe(); }, []); const handleLogin = async (event) => { event.preventDefault(); setLoading(true); const { error } = await supabase.auth.signInWithOtp({ email, options: { emailRedirectTo: window.location.origin, } }); if (error) { alert(error.error_description || error.message); } else { alert("Check your email for the login link!"); } setLoading(false); }; const handleLogout = async () => { await supabase.auth.signOut(); setSession(null); }; // Show verification state if (verifying) { return ( <div> <h1>Authentication</h1> <p>Confirming your magic link...</p> <p>Loading...</p> </div> ); } // Show auth error if (authError) { return ( <div> <h1>Authentication</h1> <p>✗ Authentication failed</p> <p>{authError}</p> <button onClick={() => { setAuthError(null); window.history.replaceState({}, document.title, "/"); }} > Return to login </button> </div> ); } // Show auth success (briefly before session loads) if (authSuccess && !session) { return ( <div> <h1>Authentication</h1> <p>✓ Authentication successful!</p> <p>Loading your account...</p> </div> ); } // If user is logged in, show welcome screen if (session) { return ( <div> <h1>Welcome!</h1> <p>You are logged in as: {session.user.email}</p> <button onClick={handleLogout}> Sign Out </button> </div> ); } // Show login form return ( <div> <h1>Supabase + React</h1> <p>Sign in via magic link with your email below</p> <form onSubmit={handleLogin}> <input type="email" placeholder="Your email" value={email} required={true} onChange={(e) => setEmail(e.target.value)} /> <button disabled={loading}> {loading ? <span>Loading</span> : <span>Send magic link</span>} </button> </form> </div> );}Customize email template
Before proceeding, change the email template to support support a server-side authentication flow that sends a token hash:
- Go to the Auth templates page in your dashboard.
- Select the Confirm sign up template.
- Change
{{ .ConfirmationURL }}to{{ .SiteURL }}?token_hash={{ .TokenHash }}&type=email. - Change your Site URL to
https://localhost:5173
Start the app
Start the app, go to http://localhost:5173 in a browser, and open the browser console and you should be able to register and log in.
Terminal
1npm run dev