Skip to main content
RapidDev - Software Development Agency
supabase-tutorial

How to Check if a User Is Authenticated in Supabase

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.

What you'll learn

  • How to use getUser() vs getSession() and when to use each
  • How to implement client-side auth guards that redirect unauthenticated users
  • How to verify authentication on the server side
  • How to subscribe to auth state changes for reactive UI updates
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate8 min read15-20 minSupabase (all plans), @supabase/supabase-js v2+March 2026RapidDev Engineering Team
TL;DR

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

1

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).

typescript
1import { createClient } from '@supabase/supabase-js'
2
3const supabase = createClient(
4 process.env.NEXT_PUBLIC_SUPABASE_URL!,
5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
6)
7
8// TRUSTED: Makes API request, validates JWT
9const { data: { user }, error } = await supabase.auth.getUser()
10if (user) {
11 console.log('Authenticated:', user.id)
12} else {
13 console.log('Not authenticated')
14}
15
16// UNTRUSTED: Reads from local storage, fast but unverified
17const { 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.

2

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.

typescript
1// Set up a global auth state listener
2const { 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 break
8 case 'SIGNED_OUT':
9 console.log('User signed out')
10 // Redirect to login page
11 break
12 case 'TOKEN_REFRESHED':
13 console.log('Token refreshed')
14 break
15 }
16 }
17)
18
19// Clean up when no longer needed
20subscription.unsubscribe()

Expected result: Your app receives real-time notifications of auth state changes and can update the UI immediately.

3

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.

typescript
1// Reusable auth guard function
2async function requireAuth(): Promise<{ id: string; email: string }> {
3 const { data: { user }, error } = await supabase.auth.getUser()
4
5 if (error || !user) {
6 // Redirect to login — adapt to your routing framework
7 window.location.href = '/login'
8 throw new Error('Not authenticated')
9 }
10
11 return { id: user.id, email: user.email! }
12}
13
14// React hook version
15import { useEffect, useState } from 'react'
16
17function useAuth() {
18 const [user, setUser] = useState<any>(null)
19 const [loading, setLoading] = useState(true)
20
21 useEffect(() => {
22 supabase.auth.getUser().then(({ data: { user } }) => {
23 setUser(user)
24 setLoading(false)
25 })
26
27 const { data: { subscription } } = supabase.auth.onAuthStateChange(
28 (event, session) => {
29 setUser(session?.user ?? null)
30 }
31 )
32
33 return () => subscription.unsubscribe()
34 }, [])
35
36 return { user, loading }
37}

Expected result: A reusable auth guard that verifies authentication and redirects unauthorized users to the login page.

4

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.

typescript
1// Next.js App Router: Server Component auth check
2import { createServerClient } from '@supabase/ssr'
3import { cookies } from 'next/headers'
4import { redirect } from 'next/navigation'
5
6export 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 )
13
14 const { data: { user } } = await supabase.auth.getUser()
15
16 if (!user) {
17 redirect('/login')
18 }
19
20 return <div>Welcome, {user.email}</div>
21}

Expected result: The server verifies the JWT and either renders the protected content or redirects to login.

5

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.

typescript
1-- RLS ensures data security even if client-side checks are bypassed
2alter table public.todos enable row level security;
3
4create policy "Users can only see own todos"
5 on public.todos for select
6 to authenticated
7 using ((select auth.uid()) = user_id);
8
9create policy "Users can only insert own todos"
10 on public.todos for insert
11 to authenticated
12 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

auth-check.ts
1import { createClient, User } from '@supabase/supabase-js'
2
3const supabase = createClient(
4 process.env.NEXT_PUBLIC_SUPABASE_URL!,
5 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
6)
7
8// Check if user is authenticated (trusted, server-verified)
9async function isAuthenticated(): Promise<boolean> {
10 const { data: { user } } = await supabase.auth.getUser()
11 return user !== null
12}
13
14// Get the current authenticated user or null
15async function getCurrentUser(): Promise<User | null> {
16 const { data: { user } } = await supabase.auth.getUser()
17 return user
18}
19
20// Require authentication — throws if not logged in
21async 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 user
27}
28
29// React hook for reactive auth state
30import { useEffect, useState } from 'react'
31
32export function useAuth() {
33 const [user, setUser] = useState<User | null>(null)
34 const [loading, setLoading] = useState(true)
35
36 useEffect(() => {
37 // Initial check
38 supabase.auth.getUser().then(({ data: { user } }) => {
39 setUser(user)
40 setLoading(false)
41 })
42
43 // Subscribe to changes
44 const { data: { subscription } } = supabase.auth.onAuthStateChange(
45 (_event, session) => {
46 setUser(session?.user ?? null)
47 setLoading(false)
48 }
49 )
50
51 return () => subscription.unsubscribe()
52 }, [])
53
54 return { user, loading, isAuthenticated: !!user }
55}
56
57// Protected route wrapper component
58import { ReactNode } from 'react'
59
60export function ProtectedRoute({ children }: { children: ReactNode }) {
61 const { user, loading } = useAuth()
62
63 if (loading) return <div>Loading...</div>
64 if (!user) {
65 window.location.href = '/login'
66 return null
67 }
68
69 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.

ChatGPT Prompt

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.

Supabase Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.