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

How to Do Server-Side Auth Check in Supabase

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.

What you'll learn

  • Why getUser() is the only safe method for server-side auth verification
  • How to create a Supabase server client with @supabase/ssr
  • How to implement auth middleware in Next.js and Express
  • How to protect API routes and server actions from unauthorized access
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+, @supabase/ssr v0.5+March 2026RapidDev Engineering Team
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.

Prerequisites

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

typescript
1// UNSAFE on the server — reads cached session without verification
2const { data: { session } } = await supabase.auth.getSession()
3// session.user could be spoofed!
4
5// SAFE on the server — verifies JWT with Supabase Auth server
6const { data: { user }, error } = await supabase.auth.getUser()
7// 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.

typescript
1// lib/supabase/server.ts (for Next.js App Router)
2import { createServerClient } from '@supabase/ssr'
3import { cookies } from 'next/headers'
4
5export async function createClient() {
6 const cookieStore = await cookies()
7
8 return createServerClient(
9 process.env.NEXT_PUBLIC_SUPABASE_URL!,
10 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
11 {
12 cookies: {
13 getAll() {
14 return cookieStore.getAll()
15 },
16 setAll(cookiesToSet) {
17 try {
18 cookiesToSet.forEach(({ name, value, options }) =>
19 cookieStore.set(name, value, options)
20 )
21 } catch {
22 // setAll may fail in Server Components
23 }
24 },
25 },
26 }
27 )
28}

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.

typescript
1// app/dashboard/page.tsx (Next.js Server Component)
2import { createClient } from '@/lib/supabase/server'
3import { redirect } from 'next/navigation'
4
5export default async function DashboardPage() {
6 const supabase = await createClient()
7 const { data: { user }, error } = await supabase.auth.getUser()
8
9 if (error || !user) {
10 redirect('/login')
11 }
12
13 // User is verified — safe to fetch user-specific data
14 const { data: todos } = await supabase
15 .from('todos')
16 .select('*')
17 .eq('user_id', user.id)
18
19 return (
20 <div>
21 <h1>Welcome, {user.email}</h1>
22 <ul>
23 {todos?.map((todo) => (
24 <li key={todo.id}>{todo.title}</li>
25 ))}
26 </ul>
27 </div>
28 )
29}

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.

typescript
1// middleware.ts (Next.js root middleware)
2import { createServerClient } from '@supabase/ssr'
3import { NextResponse } from 'next/server'
4import type { NextRequest } from 'next/server'
5
6export async function middleware(request: NextRequest) {
7 let supabaseResponse = NextResponse.next({ request })
8
9 const supabase = createServerClient(
10 process.env.NEXT_PUBLIC_SUPABASE_URL!,
11 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
12 {
13 cookies: {
14 getAll() {
15 return request.cookies.getAll()
16 },
17 setAll(cookiesToSet) {
18 cookiesToSet.forEach(({ name, value, options }) => {
19 request.cookies.set(name, value)
20 supabaseResponse.cookies.set(name, value, options)
21 })
22 },
23 },
24 }
25 )
26
27 const { data: { user } } = await supabase.auth.getUser()
28
29 // Redirect unauthenticated users to login
30 if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
31 const url = request.nextUrl.clone()
32 url.pathname = '/login'
33 return NextResponse.redirect(url)
34 }
35
36 return supabaseResponse
37}
38
39export const config = {
40 matcher: ['/dashboard/:path*', '/settings/:path*'],
41}

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.

typescript
1// app/api/todos/route.ts (Next.js API Route)
2import { createClient } from '@/lib/supabase/server'
3import { NextResponse } from 'next/server'
4
5export async function POST(request: Request) {
6 const supabase = await createClient()
7 const { data: { user }, error } = await supabase.auth.getUser()
8
9 if (error || !user) {
10 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
11 }
12
13 const body = await request.json()
14
15 // RLS ensures user can only insert their own todos
16 const { data, error: insertError } = await supabase
17 .from('todos')
18 .insert({ title: body.title, user_id: user.id })
19 .select()
20 .single()
21
22 if (insertError) {
23 return NextResponse.json({ error: insertError.message }, { status: 400 })
24 }
25
26 return NextResponse.json({ data })
27}

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

Complete working example

lib/supabase/server.ts
1// lib/supabase/server.ts
2// Server-side Supabase client for Next.js App Router
3
4import { createServerClient } from '@supabase/ssr'
5import { cookies } from 'next/headers'
6
7export async function createClient() {
8 const cookieStore = await cookies()
9
10 return createServerClient(
11 process.env.NEXT_PUBLIC_SUPABASE_URL!,
12 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
13 {
14 cookies: {
15 getAll() {
16 return cookieStore.getAll()
17 },
18 setAll(cookiesToSet) {
19 try {
20 cookiesToSet.forEach(({ name, value, options }) =>
21 cookieStore.set(name, value, options)
22 )
23 } catch {
24 // The `setAll` method may be called from a Server Component
25 // where cookies cannot be set. This is safe to ignore.
26 }
27 },
28 },
29 }
30 )
31}
32
33// Helper: get verified user or null
34export async function getAuthUser() {
35 const supabase = await createClient()
36 const { data: { user }, error } = await supabase.auth.getUser()
37 if (error || !user) return null
38 return user
39}
40
41// Helper: require auth or throw
42export async function requireAuth() {
43 const user = await getAuthUser()
44 if (!user) {
45 throw new Error('Unauthorized')
46 }
47 return user
48}

Common mistakes when doing Server-Side Auth Check in Supabase

Why it's a problem: Using getSession() on the server to check if a user is logged in

How to avoid: 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.

Why it's a problem: Reusing the same Supabase server client across multiple requests

How to avoid: Create a new server client for every request. Cookies are request-specific, so reusing a client would mix sessions between users.

Why it's a problem: Not checking for auth in API routes, relying only on RLS for protection

How to avoid: 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

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I'm building a Next.js app with Supabase. Show me how to create a server-side Supabase client with @supabase/ssr, verify the user with getUser() in Server Components and API routes, and set up middleware to redirect unauthenticated users.

Supabase Prompt

Set up server-side auth verification for my Supabase + Next.js App Router project. Create a server client helper using @supabase/ssr, middleware that protects /dashboard routes, and an API route that verifies the user before inserting a todo. Use getUser() for all auth checks.

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.

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.