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.
Prerequisites
- 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
Install the @nuxtjs/supabase module
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.
1# Install the module2npm install @nuxtjs/supabase34# nuxt.config.ts5export default defineNuxtConfig({6 modules: ['@nuxtjs/supabase'],7 supabase: {8 redirectOptions: {9 login: '/login',10 callback: '/confirm',11 exclude: ['/', '/about'],12 },13 },14})Expected result: The Supabase module is installed and configured. The Supabase client is automatically available via composables in all components and pages.
Add environment variables for Supabase credentials
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.
1# .env2SUPABASE_URL=https://your-project.supabase.co3SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Expected result: Environment variables are loaded by the Supabase module. The client connects to your Supabase project.
Fetch and display data using useSupabaseClient
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.
1<script setup lang="ts">2const client = useSupabaseClient()34const { data: posts, error } = await useAsyncData('posts', async () => {5 const { data, error } = await client6 .from('posts')7 .select('id, title, content, created_at')8 .eq('published', true)9 .order('created_at', { ascending: false })1011 if (error) throw error12 return data13})14</script>1516<template>17 <div>18 <h1>Blog Posts</h1>19 <p v-if="error">Failed to load posts.</p>20 <article v-for="post in posts" :key="post.id">21 <h2>{{ post.title }}</h2>22 <p>{{ post.content }}</p>23 </article>24 </div>25</template>Expected result: The page renders a list of published posts fetched from Supabase. Data is loaded during SSR for fast initial page loads.
Implement email/password authentication
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.
1<script setup lang="ts">2const client = useSupabaseClient()3const user = useSupabaseUser()4const email = ref('')5const password = ref('')6const errorMsg = ref('')78async function handleLogin() {9 const { error } = await client.auth.signInWithPassword({10 email: email.value,11 password: password.value,12 })13 if (error) {14 errorMsg.value = error.message15 } else {16 navigateTo('/dashboard')17 }18}1920async function handleSignup() {21 const { error } = await client.auth.signUp({22 email: email.value,23 password: password.value,24 })25 if (error) {26 errorMsg.value = error.message27 } else {28 navigateTo('/confirm')29 }30}3132async function handleLogout() {33 await client.auth.signOut()34 navigateTo('/login')35}36</script>3738<template>39 <div v-if="user">40 <p>Logged in as {{ user.email }}</p>41 <button @click="handleLogout">Log out</button>42 </div>43 <form v-else @submit.prevent="handleLogin">44 <input v-model="email" type="email" placeholder="Email" required />45 <input v-model="password" type="password" placeholder="Password" required />46 <p v-if="errorMsg" class="text-red-500">{{ errorMsg }}</p>47 <button type="submit">Log in</button>48 <button type="button" @click="handleSignup">Sign up</button>49 </form>50</template>Expected result: Users can sign up, log in, and log out. The UI reactively updates to show the authenticated state.
Insert data with RLS protection
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.
1<script setup lang="ts">2const client = useSupabaseClient()3const user = useSupabaseUser()4const title = ref('')5const content = ref('')67async function createPost() {8 if (!user.value) return910 const { data, error } = await client11 .from('posts')12 .insert({13 title: title.value,14 content: content.value,15 author_id: user.value.id,16 published: false,17 })18 .select()1920 if (error) {21 console.error('Insert failed:', error.message)22 } else {23 navigateTo(`/posts/${data[0].id}`)24 }25}26</script>Expected result: Authenticated users can create new posts. The RLS policy ensures they can only set their own user ID as the author.
Add route middleware for custom auth guards
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.
1// middleware/auth.ts2export default defineNuxtRouteMiddleware(async (to) => {3 const user = useSupabaseUser()45 if (!user.value) {6 return navigateTo('/login')7 }8})910// middleware/admin.ts11export default defineNuxtRouteMiddleware(async (to) => {12 const user = useSupabaseUser()13 const client = useSupabaseClient()1415 if (!user.value) {16 return navigateTo('/login')17 }1819 // Check admin role from profiles table20 const { data } = await client21 .from('profiles')22 .select('role')23 .eq('id', user.value.id)24 .single()2526 if (data?.role !== 'admin') {27 return navigateTo('/unauthorized')28 }29})3031// pages/admin.vue32<script setup lang="ts">33definePageMeta({34 middleware: 'admin',35})36</script>Expected result: Protected pages redirect unauthenticated users to login and non-admin users to an unauthorized page.
Complete working example
1<script setup lang="ts">2// Complete Nuxt page: List posts + create new post3// Requires: @nuxtjs/supabase module configured4// Table: public.posts with RLS enabled56const client = useSupabaseClient()7const user = useSupabaseUser()8const title = ref('')9const content = ref('')10const creating = ref(false)1112// Fetch published posts (SSR-compatible)13const { data: posts, refresh } = await useAsyncData('posts', async () => {14 const { data, error } = await client15 .from('posts')16 .select('id, title, content, created_at')17 .eq('published', true)18 .order('created_at', { ascending: false })19 .limit(20)2021 if (error) throw error22 return data23})2425// Create a new post (authenticated users only)26async function createPost() {27 if (!user.value) return28 creating.value = true2930 const { error } = await client31 .from('posts')32 .insert({33 title: title.value,34 content: content.value,35 author_id: user.value.id,36 published: true,37 })3839 creating.value = false40 if (!error) {41 title.value = ''42 content.value = ''43 await refresh()44 }45}46</script>4748<template>49 <div class="max-w-2xl mx-auto p-6">50 <h1 class="text-3xl font-bold mb-6">Posts</h1>5152 <form v-if="user" @submit.prevent="createPost" class="mb-8 space-y-4">53 <input v-model="title" placeholder="Post title" required54 class="w-full p-2 border rounded" />55 <textarea v-model="content" placeholder="Write your post..."56 class="w-full p-2 border rounded h-32" />57 <button type="submit" :disabled="creating"58 class="px-4 py-2 bg-blue-600 text-white rounded">59 {{ creating ? 'Creating...' : 'Create Post' }}60 </button>61 </form>6263 <article v-for="post in posts" :key="post.id" class="mb-6 p-4 border rounded">64 <h2 class="text-xl font-semibold">{{ post.title }}</h2>65 <p class="mt-2 text-gray-600">{{ post.content }}</p>66 <time class="text-sm text-gray-400">{{ new Date(post.created_at).toLocaleDateString() }}</time>67 </article>68 </div>69</template>Common mistakes when using Supabase with Nuxt.js
Why it's a problem: Using NUXT_PUBLIC_SUPABASE_URL instead of SUPABASE_URL, which the module does not read automatically
How to avoid: 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.
Why it's a problem: Calling useSupabaseClient outside of setup() or a composable, where it is not available
How to avoid: 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.
Why it's a problem: Forgetting RLS policies on the table, causing queries to return empty arrays without errors
How to avoid: 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
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a Nuxt 3 project and want to add Supabase for authentication and data. Walk me through installing @nuxtjs/supabase, configuring it, fetching data in a page with useAsyncData, and implementing email/password login with useSupabaseClient.
Set up @nuxtjs/supabase in my Nuxt 3 project. Configure redirectOptions to protect all routes except / and /about. Create a posts page that fetches published posts using useAsyncData and allows authenticated users to create new posts.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation