supabase
blockpassword-based-auth-react-routersource: ok · hit
Password Based Auth flow for React Router and Supabase
Password Based Auth flow for React Router and Supabase
npx shadcn add "https://supabase.com/ui/r/password-based-auth-react-router.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · light@supabase/ssr@latest@react-router/dev@latest@react-router/fs-routes@latest@supabase/supabase-js@latest
Registry dependencies
buttoncardinputlabelhttps://supabase.com/library/r/safe-next-path.json
Files11 · source cached 10/8/2026, 12:19:53 PM
registry/default/blocks/password-based-auth-react-router/app/routes/auth.confirm.tsx
import { type EmailOtpType } from '@supabase/supabase-js'
import { redirect, type LoaderFunctionArgs } from 'react-router'
import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'
export async function loader({ request }: LoaderFunctionArgs) {
const requestUrl = new URL(request.url)
const token_hash = requestUrl.searchParams.get('token_hash')
const type = requestUrl.searchParams.get('type') as EmailOtpType | null
const _next = requestUrl.searchParams.get('next')
const next = _next?.startsWith('/') ? _next : '/'
if (token_hash && type) {
const { supabase, headers } = createClient(request)
const { error } = await supabase.auth.verifyOtp({
type,
token_hash,
})
if (!error) {
return redirect(next, { headers })
} else {
return redirect(`/auth/error?error=${error?.message}`)
}
}
// redirect the user to an error page with some instructions
return redirect(`/auth/error?error=No token hash or type`)
}
registry/default/blocks/password-based-auth-react-router/app/routes/auth.error.tsx
import { useSearchParams } from 'react-router'
import { Card, CardContent, CardHeader, CardTitle } from '@/registry/default/components/ui/card'
export default function Page() {
let [searchParams] = useSearchParams()
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<div className="flex flex-col gap-6">
<Card>
<CardHeader>
<CardTitle className="font-medium text-lg tracking-normal">
Sorry, something went wrong.
</CardTitle>
</CardHeader>
<CardContent>
{searchParams?.get('error') ? (
<p className="text-sm text-muted-foreground">
Code error: {searchParams?.get('error')}
</p>
) : (
<p className="text-sm text-muted-foreground">An unspecified error occurred.</p>
)}
</CardContent>
</Card>
</div>
</div>
</div>
)
}
registry/default/blocks/password-based-auth-react-router/app/routes/forgot-password.tsx
import {
data,
Link,
redirect,
useFetcher,
useSearchParams,
type ActionFunctionArgs,
} from 'react-router'
import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await request.formData()
const email = formData.get('email') as string
const { supabase, headers } = createClient(request)
const origin = new URL(request.url).origin
// Send the actual reset password email
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${origin}/auth/confirm?next=/update-password`,
})
if (error) {
return data(
{
error: error instanceof Error ? error.message : 'An error occurred',
data: { email },
},
{ headers }
)
}
return redirect('/forgot-password?success')
}
export default function ForgotPassword() {
const fetcher = useFetcher<typeof action>()
let [searchParams] = useSearchParams()
const success = !!searchParams.has('success')
const error = fetcher.data?.error
const loading = fetcher.state === 'submitting'
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<div className="flex flex-col gap-6">
{success ? (
<Card>
<CardHeader>
<CardTitle className="font-medium text-lg tracking-normal">
Check Your Email
</CardTitle>
<CardDescription>Password reset instructions sent</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
If you registered using your email and password, you will receive a password reset
email.
</p>
</CardContent>
</Card>
) : (
<Card>
<CardHeader>
<CardTitle className="font-medium text-lg tracking-normal">
Reset Your Password
</CardTitle>
<CardDescription>
Type in your email and we'll send you a link to reset your password
</CardDescription>
</CardHeader>
<CardContent>
<fetcher.Form method="post">
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
placeholder="m@example.com"
required
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={loading}>
{loading ? 'Sending...' : 'Send reset email'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Already have an account?{' '}
<Link to="/login" className="underline underline-offset-4">
Sign in
</Link>
</div>
</fetcher.Form>
</CardContent>
</Card>
)}
</div>
</div>
</div>
)
}
registry/default/blocks/password-based-auth-react-router/app/routes/login.tsx
import { Link, redirect, useFetcher, useSearchParams, type ActionFunctionArgs } from 'react-router'
import { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path'
import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
export const action = async ({ request }: ActionFunctionArgs) => {
const { supabase, headers } = createClient(request)
const origin = new URL(request.url).origin
const formData = await request.formData()
const email = formData.get('email') as string
const password = formData.get('password') as string
const { error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) {
return {
error: error instanceof Error ? error.message : 'An error occurred',
}
}
// Update this route to redirect to an authenticated route. The user already has an active session.
return redirect(safeNextPath(formData.get('next'), '/protected', origin), { headers })
}
export default function Login() {
const fetcher = useFetcher<typeof action>()
const [searchParams] = useSearchParams()
const error = fetcher.data?.error
const loading = fetcher.state === 'submitting'
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<div className="flex flex-col gap-6">
<Card>
<CardHeader>
<CardTitle className="font-medium text-lg tracking-normal">Sign in</CardTitle>
<CardDescription>Enter your email below to sign in to your account</CardDescription>
</CardHeader>
<CardContent>
<fetcher.Form method="post">
<input type="hidden" name="next" value={searchParams.get('next') ?? ''} />
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
placeholder="m@example.com"
required
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
<Link
to="/forgot-password"
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
>
Forgot your password?
</Link>
</div>
<Input id="password" type="password" name="password" required />
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={loading}>
{loading ? 'Signing in...' : 'Sign in'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Don't have an account?{' '}
<Link to="/sign-up" className="underline underline-offset-4">
Sign up
</Link>
</div>
</fetcher.Form>
</CardContent>
</Card>
</div>
</div>
</div>
)
}
registry/default/blocks/password-based-auth-react-router/app/routes/logout.tsx
import { redirect, type ActionFunctionArgs } from 'react-router'
import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'
export async function loader({ request }: ActionFunctionArgs) {
const { supabase, headers } = createClient(request)
const { error } = await supabase.auth.signOut()
if (error) {
console.error(error)
return { success: false, error: error.message }
}
// Redirect to dashboard or home page after successful sign-in
return redirect('/', { headers })
}
registry/default/blocks/password-based-auth-react-router/app/routes/protected.tsx
import { redirect, useLoaderData, type LoaderFunctionArgs } from 'react-router'
import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'
import { Button } from '@/registry/default/components/ui/button'
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { supabase } = createClient(request)
const { data, error } = await supabase.auth.getUser()
if (error || !data?.user) {
return redirect('/login')
}
return data
}
export default function ProtectedPage() {
let data = useLoaderData<typeof loader>()
return (
<div className="flex items-center justify-center h-screen gap-2">
<p>
Hello <span className="text-primary font-semibold">{data.user.email}</span>
</p>
<a href="/logout">
<Button>Sign out</Button>
</a>
</div>
)
}
registry/default/blocks/password-based-auth-react-router/app/routes/sign-up.tsx
import { Link, redirect, useFetcher, useSearchParams, type ActionFunctionArgs } from 'react-router'
import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
export const action = async ({ request }: ActionFunctionArgs) => {
const { supabase } = createClient(request)
const url = new URL(request.url)
const origin = url.origin
const formData = await request.formData()
const email = formData.get('email') as string
const password = formData.get('password') as string
const repeatPassword = formData.get('repeat-password') as string
if (!password) {
return {
error: 'Password is required',
}
}
if (password !== repeatPassword) {
return { error: 'Passwords do not match' }
}
const { error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${origin}/protected`,
},
})
if (error) {
return { error: error.message }
}
return redirect('/sign-up?success')
}
export default function SignUp() {
const fetcher = useFetcher<typeof action>()
let [searchParams] = useSearchParams()
const success = !!searchParams.has('success')
const error = fetcher.data?.error
const loading = fetcher.state === 'submitting'
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<div className="flex flex-col gap-6">
{success ? (
<Card>
<CardHeader>
<CardTitle className="font-medium text-lg tracking-normal">
Thank you for signing up!
</CardTitle>
<CardDescription>Check your email to confirm</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
You've successfully signed up. Please check your email to confirm your
account before signing in.
</p>
</CardContent>
</Card>
) : (
<Card>
<CardHeader>
<CardTitle className="font-medium text-lg tracking-normal">Sign up</CardTitle>
<CardDescription>Create a new account</CardDescription>
</CardHeader>
<CardContent>
<fetcher.Form method="post">
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
name="email"
type="email"
placeholder="m@example.com"
required
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
</div>
<Input id="password" name="password" type="password" required />
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="repeat-password">Repeat Password</Label>
</div>
<Input id="repeat-password" name="repeat-password" type="password" required />
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={loading}>
{loading ? 'Creating an account...' : 'Sign up'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Already have an account?{' '}
<Link to="/login" className="underline underline-offset-4">
Sign in
</Link>
</div>
</fetcher.Form>
</CardContent>
</Card>
)}
</div>
</div>
</div>
)
}
registry/default/blocks/password-based-auth-react-router/app/routes/update-password.tsx
import { redirect, useFetcher, type ActionFunctionArgs } from 'react-router'
import { createClient } from '@/registry/default/clients/react-router/lib/supabase/server'
import { Button } from '@/registry/default/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
import { Input } from '@/registry/default/components/ui/input'
import { Label } from '@/registry/default/components/ui/label'
export const action = async ({ request }: ActionFunctionArgs) => {
const { supabase, headers } = createClient(request)
const formData = await request.formData()
const password = formData.get('password') as string
if (!password) {
return { error: 'Password is required' }
}
const { error } = await supabase.auth.updateUser({ password: password })
if (error) {
return {
error: error instanceof Error ? error.message : 'An error occurred',
}
}
// Redirect to sign-in page after successful password update
return redirect('/protected', { headers })
}
export default function Page() {
const fetcher = useFetcher<typeof action>()
const error = fetcher.data?.error
const loading = fetcher.state === 'submitting'
return (
<div className="flex min-h-svh w-full items-center justify-center p-6 md:p-10">
<div className="w-full max-w-sm">
<div className="flex flex-col gap-6">
<Card>
<CardHeader>
<CardTitle className="font-medium text-lg tracking-normal">
Reset Your Password
</CardTitle>
<CardDescription>Please enter your new password below.</CardDescription>
</CardHeader>
<CardContent>
<fetcher.Form method="post">
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="password">New password</Label>
<Input
id="password"
name="password"
type="password"
placeholder="New password"
required
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={loading}>
{loading ? 'Saving...' : 'Save new password'}
</Button>
</div>
</fetcher.Form>
</CardContent>
</Card>
</div>
</div>
</div>
)
}
registry/default/blocks/password-based-auth-react-router/app/routes.ts
import { type RouteConfig } from '@react-router/dev/routes'
import { flatRoutes } from '@react-router/fs-routes'
export default flatRoutes() satisfies RouteConfig
registry/default/clients/react-router/lib/supabase/client.ts
/// <reference types="vite/types/importMeta.d.ts" />
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
import.meta.env.VITE_SUPABASE_URL!,
import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!
)
}
registry/default/clients/react-router/lib/supabase/server.ts
import { createServerClient, parseCookieHeader, serializeCookieHeader } from '@supabase/ssr'
export function createClient(request: Request) {
const headers = new Headers()
const supabase = createServerClient(
process.env.VITE_SUPABASE_URL!,
process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll() {
return parseCookieHeader(request.headers.get('Cookie') ?? '') as {
name: string
value: string
}[]
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) =>
headers.append('Set-Cookie', serializeCookieHeader(name, value, options))
)
},
},
}
)
return { supabase, headers }
}