Skip to main content

Using Supabase Service Clients in Our Next.js Web App

· 5 min read

Overview

In our latest implementation, we've introduced a clear separation between public and privileged Supabase clients in our Next.js web app. This post explains the reasoning, implementation, and flow behind our new serviceClient pattern, and how it enables secure, robust backend operations—especially for tasks like recording user swipes.

What is a Supabase Service Client?

A Supabase service client is an instance of the Supabase client initialized with the service role key (sometimes called the service key). This key grants full admin privileges to your database, bypassing all Row Level Security (RLS) policies. It should only ever be used in trusted backend code—never in the browser or client-side code.

  • Public/anon client: Uses the public anon key, subject to RLS. Safe for frontend and SSR.
  • Service client: Uses the service key, bypasses RLS. Only for backend/server code.

Why Use a Service Client?

  • Bypass RLS for trusted backend operations: Some backend tasks (like recording swipes, running cron jobs, or admin actions) need to bypass RLS for reliability and flexibility.
  • Avoid RLS errors: If you use the anon key in your backend, you may hit RLS errors even for legitimate operations.
  • Security: The service key is never exposed to the frontend, so your data remains secure.

Implementation in Our Codebase

We provide a dedicated function in /src/lib/supabase/server.ts:

export function createServiceClient() {
return createServerClient<Database>(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!, {
cookies: {
getAll() {
return []
},
setAll() {
/* No-op for service client */
},
},
})
}
  • This function returns a Supabase client with the service key.
  • It is used only in API routes or backend code.
  • The cookies config is a no-op, since the service client does not need user session context.

Example: Recording a Swipe

In our /api/swipes route, we now use the service client:

import { createServiceClient } from "@/lib/supabase/server"

export async function POST(request: NextRequest) {
const supabase = createServiceClient()
// ... privileged DB operations ...
}

This ensures that when a user swipes on a movie, the backend can always insert or update the rec_swipes table, regardless of RLS policies.


Flow Diagram

  1. Frontend sends a swipe request to /api/swipes.
  2. API Route uses createServiceClient() to get a privileged Supabase client.
  3. Backend verifies the user and movie, then inserts/updates the swipe record, bypassing RLS.
  4. Response is sent back to the frontend.

Why Not Use a Singleton?

  • Supabase clients are lightweight and stateless.
  • Creating a new client per request is the recommended and safe pattern in serverless/server environments.
  • No performance or connection pooling benefit from a singleton in this context.

Comparing Supabase Client Factories: createClient, createAPIRouteClient, and createSafeClient

Here’s a detailed breakdown of the differences between these three utilities in our Supabase server code, especially regarding cookies and their use cases:

1. createClient

  • Purpose: Used in server components or SSR contexts where you want to access the user's session (auth) via cookies.
  • How cookies are handled:
    • Uses Next.js’s cookies() API to access the current request’s cookies.
    • Passes these cookies to the Supabase client so it can read the user’s session and authenticate requests as that user.
  • Use case:
    • Server-side rendering (SSR) or server components that need to act on behalf of the currently authenticated user.
    • Example: Fetching user-specific data during SSR.

2. createAPIRouteClient

  • Purpose: Used in Next.js API routes (App Router) where you want to access the user's session, but you don’t have access to the cookies() API (since API routes use the raw request object).
  • How cookies are handled:
    • Manually parses the cookie header from the incoming request.
    • Creates a cookie store object that mimics the cookies API for Supabase.
    • Passes this to the Supabase client so it can authenticate as the user making the API request.
  • Use case:
    • API routes that need to perform actions on behalf of the authenticated user.
    • Example: /api/profile route that returns the current user’s profile.

3. createSafeClient

  • Purpose: A wrapper around createClient that adds error handling for session retrieval.
  • How cookies are handled:
    • Inherits the cookie handling from createClient (i.e., uses the current request’s cookies).
  • What’s different:
    • Wraps the auth.getSession() method to catch and log errors, always returning a safe fallback if something goes wrong.
  • Use case:
    • When you want to ensure your server code never crashes due to session retrieval errors.
    • Example: SSR or server components where you want robust error handling for authentication/session logic.

Summary Table

FunctionCookie SourceUse CaseAuth Context
createClientNext.js cookies()SSR/server components (user context)Current user session
createAPIRouteClientRequest headerAPI routes (user context)Current user session
createSafeClientNext.js cookies()SSR/server components (safe session)Current user session (with error handling)

Key Point: All three are about acting as the current user by passing the right cookies/session to Supabase. The difference is in how they access cookies (depending on the context) and whether they add extra error handling.


Key Takeaways

  • Use the service client for backend operations that must bypass RLS.
  • Never expose the service key to the frontend.
  • Continue using the public/anon client for all user-facing and SSR operations.
  • This pattern keeps your app secure, robust, and maintainable.

Questions? Reach out to the team or check the Supabase docs for more on RLS and service keys.