Connecting Your Frontend to Self-Hosted Supabase: Next.js, Nuxt, and SvelteKit

Configure Supabase client SDKs for self-hosted instances with Next.js, Nuxt, and SvelteKit including SSR authentication.

Cover Image for Connecting Your Frontend to Self-Hosted Supabase: Next.js, Nuxt, and SvelteKit

You've deployed self-hosted Supabase and everything looks good from the dashboard. But when you try to connect your React or Vue application, something feels off. The examples in Supabase's documentation reference supabase.co URLs, and suddenly you're second-guessing whether you've configured things correctly.

The good news: connecting frontend frameworks to self-hosted Supabase uses the same client libraries as Supabase Cloud. The differences are subtle but important—wrong URL configuration breaks authentication flows, SSR misconfigurations leak tokens, and CORS issues can leave you debugging for hours. This guide covers the specific configuration patterns for Next.js, Nuxt, and SvelteKit that work with self-hosted deployments.

Understanding Self-Hosted URLs

Before diving into framework-specific code, let's clarify the URLs you need. Self-hosted Supabase exposes its API through a single entry point—typically port 8000 for HTTP or 8443 for HTTPS when using the default Kong configuration.

# Your self-hosted Supabase API URL (used by client SDKs)
SUPABASE_URL=https://supabase.yourdomain.com

# Your anon key (safe for frontend code)
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

The SUPABASE_URL must be the externally-reachable address of your Kong API gateway. If you've set up a reverse proxy with custom domains, use that domain. Internal Docker network URLs like http://kong:8000 won't work from browser code—those are only valid inside your server infrastructure.

Finding your anon key depends on how you deployed. Check your .env file for ANON_KEY or generate new keys using the environment variables guide. Never use the SERVICE_ROLE_KEY in frontend code—it bypasses Row Level Security and should only exist server-side.

Next.js Configuration

Next.js is the most common choice for Supabase applications, and the Supabase team provides first-class support through @supabase/ssr. Here's how to configure it for self-hosted instances.

Environment Variables

Create a .env.local file at your project root:

# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://supabase.yourdomain.com
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here

The NEXT_PUBLIC_ prefix exposes these variables to browser code. This is intentional—your Supabase URL and anon key must be accessible client-side. The anon key is safe to expose because it only grants permissions allowed by your Row Level Security policies.

Installing Dependencies

npm install @supabase/supabase-js @supabase/ssr

Creating the Supabase Client

For Next.js App Router, create utility functions that work in both server and client contexts:

// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}
// lib/supabase/server.ts
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 {
            // Ignore errors in Server Components
          }
        },
      },
    }
  )
}

Middleware for Session Refresh

Authentication sessions need refreshing. Create middleware that runs on every request:

// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, 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 }) =>
            request.cookies.set(name, value)
          )
          supabaseResponse = NextResponse.next({ request })
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  // Refresh session if expired
  await supabase.auth.getUser()

  return supabaseResponse
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

Self-Hosted Gotchas for Next.js

Cookie Domain Issues: If your frontend runs on app.yourdomain.com and Supabase runs on supabase.yourdomain.com, cookies may not be shared correctly. Ensure your custom domain setup uses the same root domain, or configure explicit cookie domains.

HTTPS Requirement: Production self-hosted Supabase must run over HTTPS. The Supabase client sets Secure cookie flags by default, which won't work over HTTP except on localhost.

Auth Redirects: When using OAuth providers, the redirect URL must point to your self-hosted instance, not supabase.co. See our OAuth providers guide for the correct redirect URI format.

Nuxt 3 Configuration

Nuxt 3 uses a module-based approach. The @nuxtjs/supabase module works with self-hosted instances—you just need to point it at your deployment.

Installing the Module

npm install @nuxtjs/supabase

Configuration

Update your nuxt.config.ts:

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/supabase'],
  
  supabase: {
    url: process.env.SUPABASE_URL,
    key: process.env.SUPABASE_ANON_KEY,
    redirectOptions: {
      login: '/login',
      callback: '/confirm',
      exclude: ['/', '/about', '/pricing'],
    },
  },
  
  runtimeConfig: {
    public: {
      supabaseUrl: process.env.SUPABASE_URL,
      supabaseKey: process.env.SUPABASE_ANON_KEY,
    },
  },
})

Environment Variables

Create a .env file:

SUPABASE_URL=https://supabase.yourdomain.com
SUPABASE_ANON_KEY=your-anon-key-here

Note that Nuxt doesn't require the NUXT_PUBLIC_ prefix when using runtimeConfig.public. The module handles this automatically.

Using the Composables

The Nuxt module provides composables that work in both client and server contexts:

<script setup lang="ts">
const client = useSupabaseClient()
const user = useSupabaseUser()

const { data: posts } = await useAsyncData('posts', async () => {
  const { data } = await client.from('posts').select('*')
  return data
})
</script>

<template>
  <div>
    <p v-if="user">Welcome, {{ user.email }}</p>
    <ul>
      <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
    </ul>
  </div>
</template>

Server-Side Database Access

For server routes and API endpoints that need elevated privileges:

// server/api/admin/users.get.ts
import { serverSupabaseServiceRole } from '#supabase/server'

export default defineEventHandler(async (event) => {
  // This uses the service role key - bypasses RLS
  // Only use for admin operations
  const client = await serverSupabaseServiceRole(event)
  
  const { data, error } = await client
    .from('profiles')
    .select('*')
    .limit(100)
  
  if (error) throw createError({ statusCode: 500, message: error.message })
  
  return data
})

For the service role to work, add it to your environment:

# .env (server-side only, never expose to client)
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key

Nuxt-Specific Considerations

Server Rendering: The Nuxt module handles SSR automatically. User sessions are read from cookies and available in useSupabaseUser() on both client and server.

Redirect Configuration: The redirectOptions in your config control automatic redirects when accessing protected routes. Make sure the callback path matches your redirect URL configuration.

SvelteKit Configuration

SvelteKit requires manual setup, but the patterns are straightforward once you understand hooks and load functions.

Installing Dependencies

npm install @supabase/supabase-js @supabase/ssr

Environment Variables

# .env
PUBLIC_SUPABASE_URL=https://supabase.yourdomain.com
PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here

Creating the Supabase Client

Set up server and browser clients:

// src/lib/supabase.ts
import { createBrowserClient, createServerClient } from '@supabase/ssr'
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'

export function createBrowserSupabaseClient() {
  return createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY)
}

export function createServerSupabaseClient(
  cookies: {
    getAll: () => Array<{ name: string; value: string }>
    setAll: (cookies: Array<{ name: string; value: string; options?: any }>) => void
  }
) {
  return createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { cookies })
}

Server Hooks for Session Management

// src/hooks.server.ts
import { createServerSupabaseClient } from '$lib/supabase'
import type { Handle } from '@sveltejs/kit'

export const handle: Handle = async ({ event, resolve }) => {
  event.locals.supabase = createServerSupabaseClient({
    getAll() {
      return event.cookies.getAll()
    },
    setAll(cookiesToSet) {
      cookiesToSet.forEach(({ name, value, options }) => {
        event.cookies.set(name, value, { ...options, path: '/' })
      })
    },
  })

  event.locals.safeGetSession = async () => {
    const {
      data: { session },
    } = await event.locals.supabase.auth.getSession()
    
    if (!session) return { session: null, user: null }

    const {
      data: { user },
      error,
    } = await event.locals.supabase.auth.getUser()
    
    if (error) return { session: null, user: null }

    return { session, user }
  }

  return resolve(event, {
    filterSerializedResponseHeaders(name) {
      return name === 'content-range' || name === 'x-supabase-api-version'
    },
  })
}

TypeScript Definitions

Extend the SvelteKit types:

// src/app.d.ts
import type { Session, SupabaseClient, User } from '@supabase/supabase-js'

declare global {
  namespace App {
    interface Locals {
      supabase: SupabaseClient
      safeGetSession: () => Promise<{ session: Session | null; user: User | null }>
    }
  }
}

export {}

Root Layout for Session State

// src/routes/+layout.server.ts
import type { LayoutServerLoad } from './$types'

export const load: LayoutServerLoad = async ({ locals: { safeGetSession } }) => {
  const { session, user } = await safeGetSession()
  return { session, user }
}
<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { onMount } from 'svelte'
  import { invalidate } from '$app/navigation'
  import { createBrowserSupabaseClient } from '$lib/supabase'

  export let data

  let supabase = createBrowserSupabaseClient()

  onMount(() => {
    const { data: subscription } = supabase.auth.onAuthStateChange((event, session) => {
      if (session?.expires_at !== data.session?.expires_at) {
        invalidate('supabase:auth')
      }
    })

    return () => subscription.subscription.unsubscribe()
  })
</script>

<slot />

Common Configuration Issues

Regardless of framework, these issues frequently trip up developers connecting to self-hosted Supabase:

Wrong URL Format

The Supabase URL must include the protocol and exclude trailing slashes:

# Correct
SUPABASE_URL=https://supabase.yourdomain.com

# Wrong - missing protocol
SUPABASE_URL=supabase.yourdomain.com

# Wrong - trailing slash
SUPABASE_URL=https://supabase.yourdomain.com/

# Wrong - including /rest/v1 path
SUPABASE_URL=https://supabase.yourdomain.com/rest/v1

CORS Errors

If you see CORS errors in your browser console, your Kong configuration may not include the correct allowed origins. Check your Kong configuration includes your frontend domain, or configure CORS headers in your reverse proxy setup.

Mixed HTTP/HTTPS

Self-hosted Supabase should run over HTTPS in production. If your frontend is HTTPS but Supabase is HTTP, modern browsers will block the requests as mixed content. Our SSL setup guide covers certificate configuration.

Authentication Redirect Loops

When using OAuth or magic links, incorrect redirect URL configuration causes infinite loops. The SITE_URL environment variable in your Supabase deployment must match your frontend URL exactly. See the redirect URL configuration guide for troubleshooting steps.

Testing Your Connection

Before building your full application, verify the connection works:

// Quick test in any framework
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,  // Or your framework's equivalent
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)

// Test database connection
const { data, error } = await supabase.from('any_table').select('*').limit(1)

if (error) {
  console.error('Connection failed:', error.message)
} else {
  console.log('Connection successful!')
}

If this fails, check:

  1. Your Supabase instance is running (docker compose ps)
  2. Kong is accessible at your configured URL
  3. The anon key matches what's in your Supabase .env
  4. Network/firewall rules allow traffic to your Supabase port

Security Considerations

When connecting frontend applications to self-hosted Supabase, keep these security practices in mind:

Never expose the service role key: It should only exist in server-side code and environment variables that aren't bundled into client code.

Enable Row Level Security: Self-hosted Supabase doesn't enforce RLS by default on new tables. Every table accessible via the API should have appropriate policies. See our RLS guide.

Use environment variables: Don't hardcode URLs or keys. Use your framework's environment variable system.

Validate on the server: Even with RLS, validate user input in your server-side code before database operations.

Further Reading

Self-hosted Supabase works seamlessly with modern frontend frameworks once configured correctly. The key is understanding that the only real difference from Supabase Cloud is the URL—everything else, from authentication flows to real-time subscriptions, works identically. With your frontend connected, you're ready to build your application with the full power of Postgres and complete control over your infrastructure.

Ready to simplify your self-hosted Supabase deployment? Supascale handles the operational complexity—backups, custom domains, OAuth configuration—so you can focus on building your application.