Skip to main content

Use Supabase Auth with React

Quickstart

  1. 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.

     select * from auth.users;
  2. Create a React app

    Create a React app using a Vite template.

    npm create vite@latest my-app -- --template react
  3. Install the Supabase client library

    Navigate to the React app and install the Supabase libraries.

    cd my-app && npm install @supabase/supabase-js
  4. Declare Supabase Environment Variables

    Rename .env.example to .env.local and populate with your Supabase connection variables:

    Get API details

    To interact with data in database tables, you use the client libraries that wrap the auto-generated Data API endpoints, authenticating using the Project URL and key from the project Connect dialog.

    Project URL: https://<project-ref>.supabase.co (find yours in the dashboard)

    Publishable key: sb_publishable_... (find yours in the dashboard)

    Note

    See API keys for a full explanation of all key types, their uses, and where to find them.

    VITE_SUPABASE_URL=your-project-url
    VITE_SUPABASE_PUBLISHABLE_KEY=your-publishable-key-or-anon-key
  5. Set up your sign-in 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 Components

    In App.jsx, create a Supabase client using your Project URL and key.

    The code uses the getClaims method in App.jsx to validate the local JWT before showing the signed-in user.

    import './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_KEY
    )
    
    export default function App() {
      const [loading, setLoading] = useState(false)
      const [email, setEmail] = useState('')
      const [claims, setClaims] = 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 using getClaims
        supabase.auth.getClaims().then(({ data: { claims } }) => {
          setClaims(claims)
        })
    
        // Listen for auth changes
        const {
          data: { subscription },
        } = supabase.auth.onAuthStateChange(() => {
          supabase.auth.getClaims().then(({ data: { claims } }) => {
            setClaims(claims)
          })
        })
    
        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()
        setClaims(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 claims load)
      if (authSuccess && !claims) {
        return (
          <div>
            <h1>Authentication</h1>
            <p>✓ Authentication successful!</p>
            <p>Loading your account...</p>
          </div>
        )
      }
    
      // If user is logged in, show welcome screen
      if (claims) {
        return (
          <div>
            <h1>Welcome!</h1>
            <p>You are logged in as: {claims.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>
      )
    }
  6. Customize email template

    Before proceeding, change the email template to 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
  7. 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 sign in.

    npm run dev