# How to Do Server-Side Auth Check in Supabase

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

## TL;DR

To verify authentication server-side in Supabase, always use getUser() instead of getSession(). The getUser() method makes an API call that validates the JWT against the Supabase Auth server, while getSession() only reads from local storage or cookies without verification. For SSR frameworks like Next.js, use @supabase/ssr to create a server client that reads session cookies, then call getUser() in middleware or API routes to protect endpoints.

## Server-Side Authentication Verification in Supabase

Server-side auth checks are critical for protecting API routes, server actions, and rendered pages from unauthorized access. The key principle is simple: never trust getSession() on the server. It reads cached data from cookies without verifying the JWT, meaning a tampered or expired token could pass validation. This tutorial shows how to use getUser() with @supabase/ssr to build secure server-side auth checks in Next.js, Express, and Edge Functions.

## Before you start

- A Supabase project with Auth configured
- The @supabase/supabase-js and @supabase/ssr packages installed
- A server-side framework (Next.js, Express, SvelteKit, etc.)
- Basic understanding of JWTs and session management

## Step-by-step guide

### 1. Understand why getSession() is unsafe on the server

The supabase.auth.getSession() method reads the session from local storage (in browsers) or cookies (in SSR). It does not make a network request to verify the JWT. This means a malicious user could modify the JWT payload in their cookies and getSession() would still return it as valid. On the server, you must use getUser() which sends the JWT to the Supabase Auth server for verification. If the JWT is tampered, expired, or revoked, getUser() returns an error.

```
// UNSAFE on the server — reads cached session without verification
const { data: { session } } = await supabase.auth.getSession()
// session.user could be spoofed!

// SAFE on the server — verifies JWT with Supabase Auth server
const { data: { user }, error } = await supabase.auth.getUser()
// user is verified, or error is returned if JWT is invalid
```

**Expected result:** You understand that getUser() is the only trusted method for server-side auth and will use it in all server contexts.

### 2. Create a Supabase server client with @supabase/ssr

The @supabase/ssr package creates a Supabase client that reads and writes session tokens via cookies instead of localStorage. This is required for server-side rendering frameworks where localStorage is not available. Create a helper function that initializes the server client with cookie handlers specific to your framework.

```
// lib/supabase/server.ts (for Next.js App Router)
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // setAll may fail in Server Components
          }
        },
      },
    }
  )
}
```

**Expected result:** A createClient() helper that returns a Supabase server client with cookie-based session handling.

### 3. Protect a Next.js Server Component with getUser()

In Next.js Server Components, create the Supabase server client and call getUser() to verify the authenticated user. If there is no valid user, redirect to the login page. This check runs on every request, ensuring that only authenticated users can see the page content.

```
// app/dashboard/page.tsx (Next.js Server Component)
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'

export default async function DashboardPage() {
  const supabase = await createClient()
  const { data: { user }, error } = await supabase.auth.getUser()

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

  // User is verified — safe to fetch user-specific data
  const { data: todos } = await supabase
    .from('todos')
    .select('*')
    .eq('user_id', user.id)

  return (
    <div>
      <h1>Welcome, {user.email}</h1>
      <ul>
        {todos?.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    </div>
  )
}
```

**Expected result:** Unauthenticated users are redirected to login. Authenticated users see their own data, filtered by RLS.

### 4. Implement auth middleware for route protection

Instead of checking auth in every page, create middleware that runs before the page renders. In Next.js, middleware runs on every request and can redirect unauthenticated users before the Server Component even loads. This is more efficient and ensures consistent auth checks across all protected routes.

```
// middleware.ts (Next.js root middleware)
import { createServerClient } from '@supabase/ssr'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  let supabaseResponse = NextResponse.next({ request })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) => {
            request.cookies.set(name, value)
            supabaseResponse.cookies.set(name, value, options)
          })
        },
      },
    }
  )

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

  // Redirect unauthenticated users to login
  if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
    const url = request.nextUrl.clone()
    url.pathname = '/login'
    return NextResponse.redirect(url)
  }

  return supabaseResponse
}

export const config = {
  matcher: ['/dashboard/:path*', '/settings/:path*'],
}
```

**Expected result:** All routes under /dashboard and /settings require authentication. Unauthenticated users are redirected before the page loads.

### 5. Protect an API route or Server Action

API routes and Server Actions also need auth verification. Create the Supabase server client, call getUser(), and return a 401 response if the user is not authenticated. This protects data mutations from being called by unauthenticated users, even if the client-side UI is bypassed.

```
// app/api/todos/route.ts (Next.js API Route)
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'

export async function POST(request: Request) {
  const supabase = await createClient()
  const { data: { user }, error } = await supabase.auth.getUser()

  if (error || !user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const body = await request.json()

  // RLS ensures user can only insert their own todos
  const { data, error: insertError } = await supabase
    .from('todos')
    .insert({ title: body.title, user_id: user.id })
    .select()
    .single()

  if (insertError) {
    return NextResponse.json({ error: insertError.message }, { status: 400 })
  }

  return NextResponse.json({ data })
}
```

**Expected result:** API routes return 401 for unauthenticated requests and process mutations only for verified users.

## Complete code example

File: `lib/supabase/server.ts`

```typescript
// lib/supabase/server.ts
// Server-side Supabase client for Next.js App Router

import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // The `setAll` method may be called from a Server Component
            // where cookies cannot be set. This is safe to ignore.
          }
        },
      },
    }
  )
}

// Helper: get verified user or null
export async function getAuthUser() {
  const supabase = await createClient()
  const { data: { user }, error } = await supabase.auth.getUser()
  if (error || !user) return null
  return user
}

// Helper: require auth or throw
export async function requireAuth() {
  const user = await getAuthUser()
  if (!user) {
    throw new Error('Unauthorized')
  }
  return user
}
```

## Common mistakes

- **Using getSession() on the server to check if a user is logged in** — undefined Fix: Always use getUser() on the server. getSession() reads from cookies without JWT verification, meaning a tampered token would pass. getUser() makes an API call to the Supabase Auth server to verify the token.
- **Reusing the same Supabase server client across multiple requests** — undefined Fix: Create a new server client for every request. Cookies are request-specific, so reusing a client would mix sessions between users.
- **Not checking for auth in API routes, relying only on RLS for protection** — undefined Fix: RLS is a database-level safety net, but your API routes should still verify the user before processing the request. Return 401 for unauthenticated users instead of letting them hit the database.

## Best practices

- Always use getUser() for server-side auth — never trust getSession() on the server
- Create a new Supabase server client for every request using @supabase/ssr
- Use middleware to protect groups of routes instead of checking auth in every page individually
- Return 401 Unauthorized from API routes before processing any business logic
- Combine server-side auth checks with RLS for defense in depth
- Use the anon key on the server (not the service role key) so RLS policies are applied to queries
- Create helper functions like getAuthUser() and requireAuth() to avoid repeating auth logic

## Frequently asked questions

### Why is getSession() not safe on the server?

getSession() reads the JWT from cookies or local storage without making a network request to verify it. A malicious user could modify the JWT payload in their cookies and getSession() would return the tampered data as if it were valid. getUser() makes an API call to the Supabase Auth server to cryptographically verify the JWT.

### Does getUser() add latency to every request?

Yes, getUser() makes a network call to the Supabase Auth server, which adds a small amount of latency (typically 20-50ms). This is the cost of security. For most applications, this latency is negligible compared to the risk of accepting unverified tokens.

### Should I use the service role key for server-side queries?

No. Use the anon key so that RLS policies are applied to your queries. The service role key bypasses all RLS, which means a bug in your code could expose data from other users. Only use the service role key for admin operations that intentionally need to bypass RLS.

### How do I protect Server Actions in Next.js?

Create the Supabase server client at the start of the Server Action and call getUser(). If the user is not authenticated, throw an error or return early. This runs on the server before any database operations.

### Can I cache the getUser() result across multiple queries in the same request?

Yes, call getUser() once at the beginning of your request handler and store the result. All subsequent queries in the same request can use the verified user object without calling getUser() again.

### Do I still need RLS if I check auth in my API routes?

Yes. RLS provides defense in depth. If a bug in your application code bypasses the auth check, RLS ensures the database still enforces access control. Always use both application-level auth checks and database-level RLS.

### Can RapidDev help implement server-side auth for my Supabase application?

Yes, RapidDev can set up secure server-side auth with @supabase/ssr, implement middleware for route protection, configure RLS policies, and build a complete auth flow for your framework of choice.

---

Source: https://www.rapidevelopers.com/supabase-tutorial/how-to-do-server-side-auth-check-in-supabase
© RapidDev — https://www.rapidevelopers.com/supabase-tutorial/how-to-do-server-side-auth-check-in-supabase
