# How to Use Supabase with Nuxt.js

- Tool: Supabase
- Difficulty: Beginner
- Time required: 15-20 min
- Compatibility: Nuxt 3+, @nuxtjs/supabase v1+, @supabase/supabase-js v2+
- Last updated: March 2026

## TL;DR

To use Supabase with Nuxt.js, install the @nuxtjs/supabase module and add your Supabase URL and anon key to nuxt.config.ts. The module provides useSupabaseClient and useSupabaseUser composables that work in both server and client contexts. Use these composables in your pages and components to perform CRUD operations, handle authentication, and listen for real-time updates without manual client initialization.

## Setting Up Supabase in a Nuxt.js Application

Nuxt.js has an official Supabase module that handles client initialization, cookie-based session management for SSR, and provides composables for data fetching and auth. This tutorial walks through installing the module, configuring it, building a data-fetching page, implementing email/password auth, and protecting routes with middleware.

## Before you start

- A Nuxt 3 project created with npx nuxi init
- A Supabase project with at least one table
- Node.js 18+ installed
- SUPABASE_URL and SUPABASE_KEY from the Supabase Dashboard (Settings > API)

## Step-by-step guide

### 1. Install the @nuxtjs/supabase module

The @nuxtjs/supabase module is the official Nuxt integration for Supabase. It auto-configures the Supabase client with cookie-based session storage for SSR, provides Vue composables, and handles auth redirects. Install it as a dependency and add it to the modules array in nuxt.config.ts.

```
# Install the module
npm install @nuxtjs/supabase

# nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@nuxtjs/supabase'],
  supabase: {
    redirectOptions: {
      login: '/login',
      callback: '/confirm',
      exclude: ['/', '/about'],
    },
  },
})
```

**Expected result:** The Supabase module is installed and configured. The Supabase client is automatically available via composables in all components and pages.

### 2. Add environment variables for Supabase credentials

Create a .env file in the project root with your Supabase URL and anon key. The @nuxtjs/supabase module reads SUPABASE_URL and SUPABASE_KEY automatically — you do not need the NUXT_PUBLIC_ prefix for these specific variables. Find these values in the Supabase Dashboard under Settings > API.

```
# .env
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

**Expected result:** Environment variables are loaded by the Supabase module. The client connects to your Supabase project.

### 3. Fetch and display data using useSupabaseClient

Use the useSupabaseClient composable to get a typed Supabase client in any component or page. Combine it with Nuxt's useAsyncData for SSR-compatible data fetching. The data is fetched on the server during SSR and hydrated on the client, providing fast initial loads. Remember that the table must have RLS policies that allow the current user (or anon role) to read the data.

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

const { data: posts, error } = await useAsyncData('posts', async () => {
  const { data, error } = await client
    .from('posts')
    .select('id, title, content, created_at')
    .eq('published', true)
    .order('created_at', { ascending: false })

  if (error) throw error
  return data
})
</script>

<template>
  <div>
    <h1>Blog Posts</h1>
    <p v-if="error">Failed to load posts.</p>
    <article v-for="post in posts" :key="post.id">
      <h2>{{ post.title }}</h2>
      <p>{{ post.content }}</p>
    </article>
  </div>
</template>
```

**Expected result:** The page renders a list of published posts fetched from Supabase. Data is loaded during SSR for fast initial page loads.

### 4. Implement email/password authentication

Use useSupabaseClient to call auth methods and useSupabaseUser to reactively track the current user. The module handles session persistence via cookies automatically. Build a login form that calls signInWithPassword and a signup form that calls signUp. The useSupabaseUser composable returns a reactive ref that updates when the auth state changes.

```
<script setup lang="ts">
const client = useSupabaseClient()
const user = useSupabaseUser()
const email = ref('')
const password = ref('')
const errorMsg = ref('')

async function handleLogin() {
  const { error } = await client.auth.signInWithPassword({
    email: email.value,
    password: password.value,
  })
  if (error) {
    errorMsg.value = error.message
  } else {
    navigateTo('/dashboard')
  }
}

async function handleSignup() {
  const { error } = await client.auth.signUp({
    email: email.value,
    password: password.value,
  })
  if (error) {
    errorMsg.value = error.message
  } else {
    navigateTo('/confirm')
  }
}

async function handleLogout() {
  await client.auth.signOut()
  navigateTo('/login')
}
</script>

<template>
  <div v-if="user">
    <p>Logged in as {{ user.email }}</p>
    <button @click="handleLogout">Log out</button>
  </div>
  <form v-else @submit.prevent="handleLogin">
    <input v-model="email" type="email" placeholder="Email" required />
    <input v-model="password" type="password" placeholder="Password" required />
    <p v-if="errorMsg" class="text-red-500">{{ errorMsg }}</p>
    <button type="submit">Log in</button>
    <button type="button" @click="handleSignup">Sign up</button>
  </form>
</template>
```

**Expected result:** Users can sign up, log in, and log out. The UI reactively updates to show the authenticated state.

### 5. Insert data with RLS protection

When inserting data into Supabase from a Nuxt component, the request uses the authenticated user's JWT. RLS policies on the table determine whether the insert is allowed. Always include the user's ID as the owner field so the RLS policy can verify the user owns the record they are creating.

```
<script setup lang="ts">
const client = useSupabaseClient()
const user = useSupabaseUser()
const title = ref('')
const content = ref('')

async function createPost() {
  if (!user.value) return

  const { data, error } = await client
    .from('posts')
    .insert({
      title: title.value,
      content: content.value,
      author_id: user.value.id,
      published: false,
    })
    .select()

  if (error) {
    console.error('Insert failed:', error.message)
  } else {
    navigateTo(`/posts/${data[0].id}`)
  }
}
</script>
```

**Expected result:** Authenticated users can create new posts. The RLS policy ensures they can only set their own user ID as the author.

### 6. Add route middleware for custom auth guards

While the module's redirectOptions handle basic auth guards, you may need custom logic for specific routes. Create Nuxt middleware files that check auth state and redirect based on user roles or other conditions. Use defineNuxtRouteMiddleware and apply it to specific pages.

```
// middleware/auth.ts
export default defineNuxtRouteMiddleware(async (to) => {
  const user = useSupabaseUser()

  if (!user.value) {
    return navigateTo('/login')
  }
})

// middleware/admin.ts
export default defineNuxtRouteMiddleware(async (to) => {
  const user = useSupabaseUser()
  const client = useSupabaseClient()

  if (!user.value) {
    return navigateTo('/login')
  }

  // Check admin role from profiles table
  const { data } = await client
    .from('profiles')
    .select('role')
    .eq('id', user.value.id)
    .single()

  if (data?.role !== 'admin') {
    return navigateTo('/unauthorized')
  }
})

// pages/admin.vue
<script setup lang="ts">
definePageMeta({
  middleware: 'admin',
})
</script>
```

**Expected result:** Protected pages redirect unauthenticated users to login and non-admin users to an unauthorized page.

## Complete code example

File: `pages/posts/index.vue`

```vue
<script setup lang="ts">
// Complete Nuxt page: List posts + create new post
// Requires: @nuxtjs/supabase module configured
// Table: public.posts with RLS enabled

const client = useSupabaseClient()
const user = useSupabaseUser()
const title = ref('')
const content = ref('')
const creating = ref(false)

// Fetch published posts (SSR-compatible)
const { data: posts, refresh } = await useAsyncData('posts', async () => {
  const { data, error } = await client
    .from('posts')
    .select('id, title, content, created_at')
    .eq('published', true)
    .order('created_at', { ascending: false })
    .limit(20)

  if (error) throw error
  return data
})

// Create a new post (authenticated users only)
async function createPost() {
  if (!user.value) return
  creating.value = true

  const { error } = await client
    .from('posts')
    .insert({
      title: title.value,
      content: content.value,
      author_id: user.value.id,
      published: true,
    })

  creating.value = false
  if (!error) {
    title.value = ''
    content.value = ''
    await refresh()
  }
}
</script>

<template>
  <div class="max-w-2xl mx-auto p-6">
    <h1 class="text-3xl font-bold mb-6">Posts</h1>

    <form v-if="user" @submit.prevent="createPost" class="mb-8 space-y-4">
      <input v-model="title" placeholder="Post title" required
        class="w-full p-2 border rounded" />
      <textarea v-model="content" placeholder="Write your post..."
        class="w-full p-2 border rounded h-32" />
      <button type="submit" :disabled="creating"
        class="px-4 py-2 bg-blue-600 text-white rounded">
        {{ creating ? 'Creating...' : 'Create Post' }}
      </button>
    </form>

    <article v-for="post in posts" :key="post.id" class="mb-6 p-4 border rounded">
      <h2 class="text-xl font-semibold">{{ post.title }}</h2>
      <p class="mt-2 text-gray-600">{{ post.content }}</p>
      <time class="text-sm text-gray-400">{{ new Date(post.created_at).toLocaleDateString() }}</time>
    </article>
  </div>
</template>
```

## Common mistakes

- **Using NUXT_PUBLIC_SUPABASE_URL instead of SUPABASE_URL, which the module does not read automatically** — undefined Fix: The @nuxtjs/supabase module expects SUPABASE_URL and SUPABASE_KEY as environment variable names. These are configured specifically for the module and do not need the NUXT_PUBLIC_ prefix.
- **Calling useSupabaseClient outside of setup() or a composable, where it is not available** — undefined Fix: Vue composables can only be called during the setup phase. If you need the client in a utility function, pass it as a parameter or use the Nuxt plugin system.
- **Forgetting RLS policies on the table, causing queries to return empty arrays without errors** — undefined Fix: Always create RLS policies for every operation you need. Check the Supabase Dashboard for tables without policies. Empty results from a query with no error usually means an RLS policy is missing.

## Best practices

- Use useAsyncData with unique keys for SSR-compatible data fetching that avoids duplicate requests
- Rely on the module's redirectOptions for basic auth guards and use custom middleware only for complex logic
- Always use the anon key (SUPABASE_KEY) for the module configuration — never the service role key
- Create RLS policies that match your application's access patterns before writing frontend queries
- Use useSupabaseUser() to reactively track auth state instead of manually calling getUser() on every page
- Handle loading and error states in templates to provide feedback when data fetching fails
- Set up the email confirmation callback route at /confirm to handle signUp redirects

## Frequently asked questions

### Does @nuxtjs/supabase handle SSR session management automatically?

Yes, the module uses cookie-based session storage that works in both server and client contexts. Sessions are automatically shared between server-rendered pages and client-side navigation without any manual configuration.

### Can I use Supabase Realtime with the Nuxt module?

Yes. Use useSupabaseClient() to get the client and subscribe to channels with .channel().on('postgres_changes', ...).subscribe(). Set up the subscription in onMounted and clean up in onUnmounted to avoid memory leaks.

### How do I generate TypeScript types for my Supabase tables?

Run supabase gen types typescript --project-id your-project-ref > types/database.ts, then pass the generated type to useSupabaseClient<Database>() for type-safe queries.

### Why does useSupabaseUser return null on the server during SSR?

This usually means the session cookie is not being sent with the request. Ensure you are using the latest version of @nuxtjs/supabase and that your auth callback route is set up correctly at /confirm.

### Can I use the Nuxt Supabase module with Nuxt 2?

No, @nuxtjs/supabase is designed for Nuxt 3 only. For Nuxt 2, manually initialize the Supabase client using @supabase/supabase-js in a Nuxt plugin.

### Can RapidDev help integrate Supabase with my Nuxt application?

Yes, RapidDev can set up the full Supabase integration for your Nuxt app, including auth flows, data fetching patterns, real-time subscriptions, and deployment configuration.

---

Source: https://www.rapidevelopers.com/supabase-tutorial/how-to-use-supabase-with-nuxt-js
© RapidDev — https://www.rapidevelopers.com/supabase-tutorial/how-to-use-supabase-with-nuxt-js
