# Use SupaHost with React (Vite)

> Set up a React single-page app built with Vite against a SupaHost project. Install supabase-js, add env vars, query a table, track the session and add email sign-in.

Source: https://supahost.app/docs/getting-started/react · Updated 2026-11-11

A React app built with Vite runs entirely in the browser, so it talks to your SupaHost project directly with supabase-js and the public `anon` key. Row Level Security in the database decides what each user can read and write.

The examples use the `recipes` table from the [quickstart](/docs/getting-started/quickstart).

## Install

```bash
npm create vite@latest recipe-box -- --template react-ts
cd recipe-box
npm install @supabase/supabase-js
```

## Add your project keys

Vite exposes variables that start with `VITE_` to your code. Create `.env.local` with the values from your project's **Connect** tab, subtab **App**:

```bash
VITE_SUPABASE_URL=https://app-one.acme.supahost.space
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIs...
```

Everything with the `VITE_` prefix ends up in the JavaScript bundle that anyone can download. That's expected for the `anon` key. The `service_role` key must never go here.

For TypeScript, declare the variables in `src/vite-env.d.ts`:

```ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_SUPABASE_URL: string
  readonly VITE_SUPABASE_ANON_KEY: string
}
```

## Create the client

Make one client and import it wherever you need it:

```ts
// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js'

export const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_ANON_KEY
)
```

The client stores the session in `localStorage` and refreshes the access token in the background.

## Track the signed-in user

A small hook keeps the current session in React state:

```tsx
// src/useSession.ts
import { useEffect, useState } from 'react'
import type { Session } from '@supabase/supabase-js'
import { supabase } from './lib/supabase'

export function useSession() {
  const [session, setSession] = useState<Session | null>(null)

  useEffect(() => {
    supabase.auth.getSession().then(({ data }) => setSession(data.session))

    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (_event, newSession) => setSession(newSession)
    )
    return () => subscription.unsubscribe()
  }, [])

  return session
}
```

## Query and insert

```tsx
// src/App.tsx
import { useEffect, useState, type FormEvent } from 'react'
import { supabase } from './lib/supabase'
import { useSession } from './useSession'
import { SignIn } from './SignIn'

type Recipe = { id: number; title: string; minutes: number | null }

export default function App() {
  const session = useSession()
  const [recipes, setRecipes] = useState<Recipe[]>([])

  async function load() {
    const { data, error } = await supabase
      .from('recipes')
      .select('id, title, minutes')
      .order('created_at', { ascending: false })
    if (!error) setRecipes(data)
  }

  useEffect(() => { load() }, [])

  async function addRecipe(e: FormEvent<HTMLFormElement>) {
    e.preventDefault()
    const title = new FormData(e.currentTarget).get('title') as string
    const { error } = await supabase.from('recipes').insert({ title })
    if (error) alert(error.message)
    else load()
  }

  return (
    <main>
      {session ? (
        <>
          <p>Signed in as {session.user.email}</p>
          <button onClick={() => supabase.auth.signOut()}>Sign out</button>
          <form onSubmit={addRecipe}>
            <input name="title" placeholder="New recipe" required />
            <button>Add</button>
          </form>
        </>
      ) : (
        <SignIn />
      )}
      <ul>
        {recipes.map((r) => <li key={r.id}>{r.title}</li>)}
      </ul>
    </main>
  )
}
```

Anyone can read recipes because of the quickstart's select policy. Inserting only works while signed in, because the insert policy is limited to `authenticated` and checks `user_id`.

## Add sign-in

```tsx
// src/SignIn.tsx
import { useState, type FormEvent } from 'react'
import { supabase } from './lib/supabase'

export function SignIn() {
  const [email, setEmail] = useState('')
  const [password, setPassword] = useState('')
  const [message, setMessage] = useState('')

  async function signIn(e: FormEvent) {
    e.preventDefault()
    const { error } = await supabase.auth.signInWithPassword({ email, password })
    if (error) setMessage(error.message)
  }

  async function signUp() {
    const { error } = await supabase.auth.signUp({
      email,
      password,
      options: { emailRedirectTo: window.location.origin },
    })
    setMessage(error ? error.message : 'Check your inbox to confirm your email.')
  }

  return (
    <form onSubmit={signIn}>
      <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
      <input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
      <button type="submit">Sign in</button>
      <button type="button" onClick={signUp}>Create account</button>
      {message && <p>{message}</p>}
    </form>
  )
}
```

When the user clicks the link in the confirmation email, they come back to your app and supabase-js picks up the session from the URL on load.

## Allow your redirect URLs

The confirmation link only returns to addresses Auth trusts. On the project's **Auth** tab, set **Site URL** to your production URL and list your other origins under **Redirect URLs**:

```text
http://localhost:5173/**
https://recipes.example.com/**
```

Click **Save Auth settings**. Changes apply within about a minute.

## Next steps

- [Social login (OAuth)](/docs/auth/social-login) to add Google or GitHub buttons
- [Realtime on SupaHost](/docs/realtime/overview) to update the list live
- [Uploading files](/docs/storage/uploads)
- [Generating TypeScript types](/docs/api/generating-types) to type `supabase.from()` calls
