# How to Check if an User Is Authenticated in Supabase

- Tool: Supabase
- Difficulty: Intermediate
- Time required: 15-20 min
- Compatibility: Supabase (all plans), @supabase/supabase-js v2+
- Last updated: March 2026

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

## Before you start

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

```
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

// TRUSTED: Makes API request, validates JWT
const { data: { user }, error } = await supabase.auth.getUser()
if (user) {
  console.log('Authenticated:', user.id)
} else {
  console.log('Not authenticated')
}

// UNTRUSTED: Reads from local storage, fast but unverified
const { data: { session } } = await supabase.auth.getSession()
if (session) {
  console.log('Session exists (unverified):', session.user.id)
}
```

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

```
// Set up a global auth state listener
const { data: { subscription } } = supabase.auth.onAuthStateChange(
  (event, session) => {
    switch (event) {
      case 'SIGNED_IN':
        console.log('User signed in:', session?.user.id)
        break
      case 'SIGNED_OUT':
        console.log('User signed out')
        // Redirect to login page
        break
      case 'TOKEN_REFRESHED':
        console.log('Token refreshed')
        break
    }
  }
)

// Clean up when no longer needed
subscription.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.

```
// Reusable auth guard function
async function requireAuth(): Promise<{ id: string; email: string }> {
  const { data: { user }, error } = await supabase.auth.getUser()

  if (error || !user) {
    // Redirect to login — adapt to your routing framework
    window.location.href = '/login'
    throw new Error('Not authenticated')
  }

  return { id: user.id, email: user.email! }
}

// React hook version
import { useEffect, useState } from 'react'

function useAuth() {
  const [user, setUser] = useState<any>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    supabase.auth.getUser().then(({ data: { user } }) => {
      setUser(user)
      setLoading(false)
    })

    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (event, session) => {
        setUser(session?.user ?? null)
      }
    )

    return () => subscription.unsubscribe()
  }, [])

  return { user, loading }
}
```

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

```
// Next.js App Router: Server Component auth check
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'

export default async function ProtectedPage() {
  const cookieStore = await cookies()
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    { cookies: { getAll: () => cookieStore.getAll() } }
  )

  const { data: { user } } = await supabase.auth.getUser()

  if (!user) {
    redirect('/login')
  }

  return <div>Welcome, {user.email}</div>
}
```

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

```
-- RLS ensures data security even if client-side checks are bypassed
alter table public.todos enable row level security;

create policy "Users can only see own todos"
  on public.todos for select
  to authenticated
  using ((select auth.uid()) = user_id);

create policy "Users can only insert own todos"
  on public.todos for insert
  to authenticated
  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 code example

File: `auth-check.ts`

```typescript
import { createClient, User } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

// Check if user is authenticated (trusted, server-verified)
async function isAuthenticated(): Promise<boolean> {
  const { data: { user } } = await supabase.auth.getUser()
  return user !== null
}

// Get the current authenticated user or null
async function getCurrentUser(): Promise<User | null> {
  const { data: { user } } = await supabase.auth.getUser()
  return user
}

// Require authentication — throws if not logged in
async function requireAuth(): Promise<User> {
  const { data: { user }, error } = await supabase.auth.getUser()
  if (error || !user) {
    throw new Error('Authentication required')
  }
  return user
}

// React hook for reactive auth state
import { useEffect, useState } from 'react'

export function useAuth() {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    // Initial check
    supabase.auth.getUser().then(({ data: { user } }) => {
      setUser(user)
      setLoading(false)
    })

    // Subscribe to changes
    const { data: { subscription } } = supabase.auth.onAuthStateChange(
      (_event, session) => {
        setUser(session?.user ?? null)
        setLoading(false)
      }
    )

    return () => subscription.unsubscribe()
  }, [])

  return { user, loading, isAuthenticated: !!user }
}

// Protected route wrapper component
import { ReactNode } from 'react'

export function ProtectedRoute({ children }: { children: ReactNode }) {
  const { user, loading } = useAuth()

  if (loading) return <div>Loading...</div>
  if (!user) {
    window.location.href = '/login'
    return null
  }

  return <>{children}</>
}
```

## Common mistakes

- **Using getSession() for authorization decisions, which reads from local storage and can be tampered with** — undefined Fix: 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.
- **Not cleaning up the onAuthStateChange subscription, causing memory leaks in single-page apps** — undefined Fix: Always call subscription.unsubscribe() in cleanup functions. In React, return it from useEffect. In Angular, call it in ngOnDestroy.
- **Relying only on client-side auth guards without RLS policies, leaving data accessible via direct API calls** — undefined Fix: Always enable RLS on your tables and write policies using auth.uid(). Client-side guards protect the UI; RLS protects the data.
- **Trusting getSession() on the server in SSR frameworks, where local storage does not exist** — undefined Fix: 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

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

---

Source: https://www.rapidevelopers.com/supabase-tutorial/how-to-check-if-a-user-is-authenticated-in-supabase
© RapidDev — https://www.rapidevelopers.com/supabase-tutorial/how-to-check-if-a-user-is-authenticated-in-supabase
