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.
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.
Install
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:
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:
/// <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:
// 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:
// 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
// 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
// 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:
http://localhost:5173/**
https://recipes.example.com/**
Click Save Auth settings. Changes apply within about a minute.
Next steps
- Social login (OAuth) to add Google or GitHub buttons
- Realtime on SupaHost to update the list live
- Uploading files
- Generating TypeScript types to type
supabase.from()calls