supabase
blockpassword-based-auth-tanstacksource: ok · hit
Password Based Auth flow for tanstack and Supabase
Password Based Auth flow for tanstack and Supabase
npx shadcn add "https://supabase.com/ui/r/password-based-auth-tanstack.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · light@supabase/ssr@latest@supabase/supabase-js@latest
Registry dependencies
buttoncardinputlabelhttps://supabase.com/library/r/safe-next-path.json
Files16 · source cached 10/15/2026, 4:39:10 AM
registry/default/blocks/password-based-auth-tanstack/routes/login.tsx
import { createFileRoute } from '@tanstack/react-router'
import { LoginForm } from '@/registry/default/blocks/password-based-auth-tanstack/components/login-form'
export const Route = createFileRoute('/login')({
component: Login,
})
function Login() {
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">
<LoginForm />
</div>
</div>
)
}
registry/default/blocks/password-based-auth-tanstack/routes/auth/error.tsx
import { createFileRoute } from '@tanstack/react-router'
import { Card, CardContent, CardHeader, CardTitle } from '@/registry/default/components/ui/card'
export const Route = createFileRoute('/auth/error')({
component: AuthError,
validateSearch: (params) => {
if (params.error && typeof params.error === 'string') {
return { error: params.error }
}
return null
},
})
function AuthError() {
const params = Route.useSearch()
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>
{params?.error ? (
<p className="text-sm text-muted-foreground">Code error: {params.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-tanstack/routes/_protected.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
import { fetchUser } from '@/registry/default/blocks/password-based-auth-tanstack/lib/supabase/fetch-user-server-fn'
export const Route = createFileRoute('/_protected')({
beforeLoad: async ({ location }) => {
const user = await fetchUser()
if (!user) {
throw redirect({ href: `/login?next=${encodeURIComponent(location.href)}` })
}
return {
user,
}
},
})
registry/default/blocks/password-based-auth-tanstack/routes/_protected/protected.tsx
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_protected/protected')({
component: Info,
loader: async ({ context }) => {
return {
user: context.user!,
}
},
})
function Info() {
const data = Route.useLoaderData()
return <p>Hello {data.user.email}</p>
}
registry/default/blocks/password-based-auth-tanstack/routes/auth/confirm.ts
import { type EmailOtpType } from '@supabase/supabase-js'
import { createFileRoute, redirect } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
import { getRequest } from '@tanstack/react-start/server'
import { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path'
import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/server'
const confirmFn = createServerFn({ method: 'GET' })
.inputValidator((searchParams: unknown) => {
if (
searchParams &&
typeof searchParams === 'object' &&
'token_hash' in searchParams &&
'type' in searchParams &&
'next' in searchParams
) {
return searchParams
}
throw new Error('Invalid search params')
})
.handler(async (ctx) => {
const request = getRequest()
if (!request) {
throw redirect({ to: `/auth/error`, search: { error: 'No request' } })
}
const searchParams = ctx.data
const token_hash = searchParams['token_hash'] as string
const type = searchParams['type'] as EmailOtpType | null
const _next = searchParams['next'] as string
const origin = new URL(request.url).origin
const next = safeNextPath(
_next?.startsWith(`${origin}/`) ? _next.slice(origin.length) : _next,
'/',
origin
)
if (token_hash && type) {
const supabase = createClient()
const { error } = await supabase.auth.verifyOtp({
type,
token_hash,
})
if (!error) {
// redirect user to specified redirect URL or root of app
throw redirect({ href: next })
} else {
// redirect the user to an error page with some instructions
console.log(error?.message)
throw redirect({
to: `/auth/error`,
search: { error: error?.message },
})
}
}
// redirect the user to an error page with some instructions
throw redirect({
to: `/auth/error`,
search: { error: 'No token hash or type' },
})
})
export const Route = createFileRoute('/auth/confirm')({
preload: false,
loader: (opts) => confirmFn({ data: opts.location.search }),
})
registry/default/blocks/password-based-auth-tanstack/components/login-form.tsx
import { Link } from '@tanstack/react-router'
import { useState } from 'react'
import { cn } from '@/lib/utils'
import { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path'
import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'
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 function LoginForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault()
const supabase = createClient()
setIsLoading(true)
setError(null)
try {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
})
if (error) throw error
// Follow the `next` query parameter if it is a same-origin relative path, e.g. when
// the OAuth consent screen sent the user here to sign in first. It may point outside
// the typed route tree, so it needs a full navigation.
const next = new URLSearchParams(window.location.search).get('next')
window.location.assign(safeNextPath(next, '/protected'))
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<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>
<form onSubmit={handleLogin}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</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"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Signing in...' : 'Sign in'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Don't have an account?{' '}
<Link to="/sign-up" search className="underline underline-offset-4">
Sign up
</Link>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
registry/default/blocks/password-based-auth-tanstack/routes/sign-up.tsx
import { createFileRoute } from '@tanstack/react-router'
import { SignUpForm } from '@/registry/default/blocks/password-based-auth-tanstack/components/sign-up-form'
export const Route = createFileRoute('/sign-up')({
component: SignUp,
})
function SignUp() {
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">
<SignUpForm />
</div>
</div>
)
}
registry/default/blocks/password-based-auth-tanstack/routes/sign-up-success.tsx
import { createFileRoute } from '@tanstack/react-router'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/registry/default/components/ui/card'
export const Route = createFileRoute('/sign-up-success')({
component: SignUpSuccess,
})
function SignUpSuccess() {
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">
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>
</div>
</div>
</div>
)
}
registry/default/blocks/password-based-auth-tanstack/components/sign-up-form.tsx
import { Link, useNavigate } from '@tanstack/react-router'
import { useState } from 'react'
import { cn } from '@/lib/utils'
import { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path'
import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'
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 function SignUpForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [repeatPassword, setRepeatPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const navigate = useNavigate()
const handleSignUp = async (e: React.FormEvent) => {
e.preventDefault()
const supabase = createClient()
setError(null)
if (password !== repeatPassword) {
setError('Passwords do not match')
return
}
setIsLoading(true)
try {
const next = safeNextPath(
new URLSearchParams(window.location.search).get('next'),
'/protected'
)
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${window.location.origin}${next}`,
},
})
if (error) throw error
if (data.session) {
window.location.assign(next)
} else {
await navigate({ to: '/sign-up-success' })
}
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="font-medium text-lg tracking-normal">Sign up</CardTitle>
<CardDescription>Create a new account</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSignUp}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="password">Password</Label>
</div>
<Input
id="password"
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="grid gap-2">
<div className="flex items-center">
<Label htmlFor="repeat-password">Repeat Password</Label>
</div>
<Input
id="repeat-password"
type="password"
required
value={repeatPassword}
onChange={(e) => setRepeatPassword(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Creating an account...' : 'Sign up'}
</Button>
</div>
<div className="mt-4 text-center text-sm">
Already have an account?{' '}
<Link to="/login" search className="underline underline-offset-4">
Sign in
</Link>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
registry/default/blocks/password-based-auth-tanstack/routes/forgot-password.tsx
import { createFileRoute } from '@tanstack/react-router'
import { ForgotPasswordForm } from '@/registry/default/blocks/password-based-auth-tanstack/components/forgot-password-form'
export const Route = createFileRoute('/forgot-password')({
component: ForgotPassword,
})
function ForgotPassword() {
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">
<ForgotPasswordForm />
</div>
</div>
)
}
registry/default/blocks/password-based-auth-tanstack/routes/update-password.tsx
import { createFileRoute } from '@tanstack/react-router'
import { UpdatePasswordForm } from '@/registry/default/blocks/password-based-auth-tanstack/components/update-password-form'
export const Route = createFileRoute('/update-password')({
component: UpdatePassword,
})
function UpdatePassword() {
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">
<UpdatePasswordForm />
</div>
</div>
)
}
registry/default/blocks/password-based-auth-tanstack/components/forgot-password-form.tsx
import { Link } from '@tanstack/react-router'
import { useState } from 'react'
import { cn } from '@/lib/utils'
import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'
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 function ForgotPasswordForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [email, setEmail] = useState('')
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const handleForgotPassword = async (e: React.FormEvent) => {
e.preventDefault()
const supabase = createClient()
setIsLoading(true)
setError(null)
try {
// The url which will be included in the email. This URL needs to be configured in your redirect URLs in the Supabase dashboard at https://supabase.com/dashboard/project/_/auth/url-configuration
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: 'http://localhost:3000/update-password',
})
if (error) throw error
setSuccess(true)
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
{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>
<form onSubmit={handleForgotPassword}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? '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>
</form>
</CardContent>
</Card>
)}
</div>
)
}
registry/default/blocks/password-based-auth-tanstack/components/update-password-form.tsx
import { useNavigate } from '@tanstack/react-router'
import { useState } from 'react'
import { cn } from '@/lib/utils'
import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/client'
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 function UpdatePasswordForm({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
const [password, setPassword] = useState('')
const [error, setError] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const navigate = useNavigate()
const handleForgotPassword = async (e: React.FormEvent) => {
e.preventDefault()
const supabase = createClient()
setIsLoading(true)
setError(null)
try {
const { error } = await supabase.auth.updateUser({ password })
if (error) throw error
// Update this route to redirect to an authenticated route. The user already has an active session.
await navigate({ to: '/protected' })
} catch (error: unknown) {
setError(error instanceof Error ? error.message : 'An error occurred')
} finally {
setIsLoading(false)
}
}
return (
<div className={cn('flex flex-col gap-6', className)} {...props}>
<Card>
<CardHeader>
<CardTitle className="font-medium text-lg tracking-normal">Reset Your Password</CardTitle>
<CardDescription>Please enter your new password below.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleForgotPassword}>
<div className="flex flex-col gap-6">
<div className="grid gap-2">
<Label htmlFor="password">New password</Label>
<Input
id="password"
type="password"
placeholder="New password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Saving...' : 'Save new password'}
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
)
}
registry/default/blocks/password-based-auth-tanstack/lib/supabase/fetch-user-server-fn.ts
import type { Factor, User } from '@supabase/supabase-js'
import { createServerFn } from '@tanstack/react-start'
import { createClient } from '@/registry/default/clients/tanstack/lib/supabase/server'
type SSRSafeUser = User & {
factors: (Factor & { factor_type: 'phone' | 'totp' })[]
}
export const fetchUser: () => Promise<SSRSafeUser | null> = createServerFn({
method: 'GET',
}).handler(async () => {
const supabase = createClient()
const { data, error } = await supabase.auth.getUser()
if (error) {
return null
}
return data.user as SSRSafeUser
})
registry/default/clients/tanstack/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/tanstack/lib/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { getCookies, setCookie } from '@tanstack/react-start/server'
export function createClient() {
return createServerClient(
process.env.VITE_SUPABASE_URL!,
process.env.VITE_SUPABASE_PUBLISHABLE_KEY!,
{
cookies: {
getAll() {
return Object.entries(getCookies()).map(
([name, value]) =>
({
name,
value,
}) as { name: string; value: string }
)
},
setAll(cookies) {
cookies.forEach((cookie) => {
setCookie(cookie.name, cookie.value)
})
},
},
}
)
}