To check if a user is authenticated in Supabase, use supabase.auth.getUser() which makes a server request to validate the JWT and returns the user object or null. For client-side route guards, subscribe to onAuthStateChange to reactively redirect unauthenticated users. Never rely solely on getSession() for authorization — it reads from local storage and can be tampered with. On the server, always use getUser() to verify the token.
Checking Authentication Status in Supabase
This tutorial covers every way to check if a user is authenticated in Supabase. You will learn the critical difference between getUser() (trusted, server-verified) and getSession() (fast but unverified), how to build client-side route guards, and how to verify auth on the server. Understanding this distinction is essential for building secure applications.
Prerequisites
- A Supabase project with auth configured
- The @supabase/supabase-js library installed
- Your Supabase URL and anon key as environment variables
- Basic knowledge of async/await in JavaScript
Step-by-step guide
Understand the difference between getUser and getSession
Understand the difference between getUser and getSession
Supabase provides two methods for checking auth status, and choosing the wrong one is a common security mistake. getSession() reads the JWT from local storage — it is fast but the data is not verified and can be tampered with. getUser() makes an actual API request to Supabase to validate the JWT and returns fresh, trusted user data. Use getSession() for quick UI checks (showing a loading state). Use getUser() for any authorization decision (protecting routes, checking permissions, server-side validation).
1import { createClient } from '@supabase/supabase-js'23const supabase = createClient(4 process.env.NEXT_PUBLIC_SUPABASE_URL!,5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!6)78// TRUSTED: Makes API request, validates JWT9const { data: { user }, error } = await supabase.auth.getUser()10if (user) {11 console.log('Authenticated:', user.id)12} else {13 console.log('Not authenticated')14}1516// UNTRUSTED: Reads from local storage, fast but unverified17const { data: { session } } = await supabase.auth.getSession()18if (session) {19 console.log('Session exists (unverified):', session.user.id)20}Expected result: getUser() returns a verified user object or null. getSession() returns a session from local storage which may be stale or tampered.
Subscribe to auth state changes for reactive checks
Subscribe to auth state changes for reactive checks
Instead of checking auth status on every page load, subscribe to onAuthStateChange to get notified whenever the user signs in, signs out, or their token refreshes. This listener fires for events like SIGNED_IN, SIGNED_OUT, TOKEN_REFRESHED, and USER_UPDATED. Set up the listener once when your app initializes and use it to update global auth state that components can read.
1// Set up a global auth state listener2const { data: { subscription } } = supabase.auth.onAuthStateChange(3 (event, session) => {4 switch (event) {5 case 'SIGNED_IN':6 console.log('User signed in:', session?.user.id)7 break8 case 'SIGNED_OUT':9 console.log('User signed out')10 // Redirect to login page11 break12 case 'TOKEN_REFRESHED':13 console.log('Token refreshed')14 break15 }16 }17)1819// Clean up when no longer needed20subscription.unsubscribe()Expected result: Your app receives real-time notifications of auth state changes and can update the UI immediately.
Build a client-side auth guard function
Build a client-side auth guard function
Create a reusable function that checks if the user is authenticated and redirects to a login page if not. This function calls getUser() for a trusted check and can be used in page components, route guards, or middleware. For React, wrap it in a custom hook. For Next.js, use it in middleware or server components.
1// Reusable auth guard function2async function requireAuth(): Promise<{ id: string; email: string }> {3 const { data: { user }, error } = await supabase.auth.getUser()45 if (error || !user) {6 // Redirect to login — adapt to your routing framework7 window.location.href = '/login'8 throw new Error('Not authenticated')9 }1011 return { id: user.id, email: user.email! }12}1314// React hook version15import { useEffect, useState } from 'react'1617function useAuth() {18 const [user, setUser] = useState<any>(null)19 const [loading, setLoading] = useState(true)2021 useEffect(() => {22 supabase.auth.getUser().then(({ data: { user } }) => {23 setUser(user)24 setLoading(false)25 })2627 const { data: { subscription } } = supabase.auth.onAuthStateChange(28 (event, session) => {29 setUser(session?.user ?? null)30 }31 )3233 return () => subscription.unsubscribe()34 }, [])3536 return { user, loading }37}Expected result: A reusable auth guard that verifies authentication and redirects unauthorized users to the login page.
Verify authentication on the server side
Verify authentication on the server side
For server-side rendering (Next.js, SvelteKit) or API routes, always verify the user's JWT on the server. Use @supabase/ssr to create a server-side Supabase client that reads the session from cookies. Call getUser() on the server to validate the JWT — never trust getSession() in server-side code. If the user is not authenticated, return a 401 response or redirect to login.
1// Next.js App Router: Server Component auth check2import { createServerClient } from '@supabase/ssr'3import { cookies } from 'next/headers'4import { redirect } from 'next/navigation'56export default async function ProtectedPage() {7 const cookieStore = await cookies()8 const supabase = createServerClient(9 process.env.NEXT_PUBLIC_SUPABASE_URL!,10 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,11 { cookies: { getAll: () => cookieStore.getAll() } }12 )1314 const { data: { user } } = await supabase.auth.getUser()1516 if (!user) {17 redirect('/login')18 }1920 return <div>Welcome, {user.email}</div>21}Expected result: The server verifies the JWT and either renders the protected content or redirects to login.
Protect data with RLS as the ultimate auth check
Protect data with RLS as the ultimate auth check
Client-side and server-side auth guards prevent users from seeing protected UI, but Row Level Security on the database is the ultimate safety net. Even if someone bypasses your client code, RLS ensures they cannot access data they do not own. Write RLS policies that use auth.uid() to restrict data access. This way, authentication is enforced at every layer: client, server, and database.
1-- RLS ensures data security even if client-side checks are bypassed2alter table public.todos enable row level security;34create policy "Users can only see own todos"5 on public.todos for select6 to authenticated7 using ((select auth.uid()) = user_id);89create policy "Users can only insert own todos"10 on public.todos for insert11 to authenticated12 with check ((select auth.uid()) = user_id);Expected result: Data is protected at the database level. Unauthenticated or unauthorized requests return empty results, not errors.
Complete working example
1import { createClient, User } from '@supabase/supabase-js'23const supabase = createClient(4 process.env.NEXT_PUBLIC_SUPABASE_URL!,5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!6)78// Check if user is authenticated (trusted, server-verified)9async function isAuthenticated(): Promise<boolean> {10 const { data: { user } } = await supabase.auth.getUser()11 return user !== null12}1314// Get the current authenticated user or null15async function getCurrentUser(): Promise<User | null> {16 const { data: { user } } = await supabase.auth.getUser()17 return user18}1920// Require authentication — throws if not logged in21async function requireAuth(): Promise<User> {22 const { data: { user }, error } = await supabase.auth.getUser()23 if (error || !user) {24 throw new Error('Authentication required')25 }26 return user27}2829// React hook for reactive auth state30import { useEffect, useState } from 'react'3132export function useAuth() {33 const [user, setUser] = useState<User | null>(null)34 const [loading, setLoading] = useState(true)3536 useEffect(() => {37 // Initial check38 supabase.auth.getUser().then(({ data: { user } }) => {39 setUser(user)40 setLoading(false)41 })4243 // Subscribe to changes44 const { data: { subscription } } = supabase.auth.onAuthStateChange(45 (_event, session) => {46 setUser(session?.user ?? null)47 setLoading(false)48 }49 )5051 return () => subscription.unsubscribe()52 }, [])5354 return { user, loading, isAuthenticated: !!user }55}5657// Protected route wrapper component58import { ReactNode } from 'react'5960export function ProtectedRoute({ children }: { children: ReactNode }) {61 const { user, loading } = useAuth()6263 if (loading) return <div>Loading...</div>64 if (!user) {65 window.location.href = '/login'66 return null67 }6869 return <>{children}</>70}Common mistakes when checkking if a User Is Authenticated in Supabase
Why it's a problem: Using getSession() for authorization decisions, which reads from local storage and can be tampered with
How to avoid: Always use getUser() for authorization. It makes an API request to validate the JWT and returns trusted data. Use getSession() only for non-security-critical UI rendering.
Why it's a problem: Not cleaning up the onAuthStateChange subscription, causing memory leaks in single-page apps
How to avoid: Always call subscription.unsubscribe() in cleanup functions. In React, return it from useEffect. In Angular, call it in ngOnDestroy.
Why it's a problem: Relying only on client-side auth guards without RLS policies, leaving data accessible via direct API calls
How to avoid: Always enable RLS on your tables and write policies using auth.uid(). Client-side guards protect the UI; RLS protects the data.
Why it's a problem: Trusting getSession() on the server in SSR frameworks, where local storage does not exist
How to avoid: On the server, use @supabase/ssr with cookie-based sessions and always call getUser() to validate the JWT.
Best practices
- Use getUser() for all authorization decisions — it validates the JWT via an API request
- Use getSession() only for fast, non-security-critical UI checks like showing a logged-in indicator
- Set up onAuthStateChange once at app initialization for reactive auth state management
- Always clean up auth subscriptions to prevent memory leaks
- Implement auth checks at every layer: client route guards, server middleware, and RLS policies
- Show a loading state while auth checks are in progress to prevent content flashing
- On the server, use @supabase/ssr with cookies instead of the browser client
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I need to check if a user is authenticated in my Supabase app. Show me the difference between getUser() and getSession(), how to build a React auth hook, and how to protect routes on both client and server side.
Help me implement authentication checks in my Supabase project. I need a React hook that tracks auth state, a protected route component, and server-side auth verification for Next.js. Show me the getUser() vs getSession() distinction.
Frequently asked questions
What is the difference between getUser() and getSession() in Supabase?
getUser() makes a server request to validate the JWT and returns trusted user data. getSession() reads from local storage without validation. Always use getUser() for security-critical checks.
Why does getSession() return a user even after the JWT has expired?
getSession() reads the cached session from local storage, which may contain an expired token. The Supabase client attempts auto-refresh, but if it fails, getSession() still returns the stale data. getUser() will correctly return null or an error.
How do I redirect unauthenticated users in Next.js?
In Next.js App Router, use redirect() from next/navigation in server components after checking getUser(). In middleware, check the session cookie and redirect if missing. In client components, use the router after checking auth state.
Can I check auth status without making a network request?
Yes, getSession() reads from local storage with no network call. But this data is unverified and should only be used for optimistic UI rendering (like showing a username), not for security decisions.
What events does onAuthStateChange emit?
It emits INITIAL_SESSION (on first load), SIGNED_IN, SIGNED_OUT, TOKEN_REFRESHED, USER_UPDATED, and PASSWORD_RECOVERY. Subscribe to these to keep your UI in sync with the auth state.
Is RLS enough or do I still need client-side auth checks?
RLS protects your data at the database level and is essential. Client-side auth checks protect the user experience by preventing access to pages they should not see. Use both for defense in depth.
Can RapidDev help implement secure authentication in my Supabase app?
Yes. RapidDev can implement complete authentication flows including auth guards, server-side verification, RLS policies, and role-based access control for your Supabase application.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation