- Published on
Complete Guide: Nuxt 3 + Supabase Auth (2025 Edition)
- Authors
- Name
- Ahmed Farid
- @
TIP
Supabase brings Firebase-like auth to Postgres. With Nuxt 3 you get SSR, file-based routing, and composables—perfect match for modern full-stack Vue apps.
This guide covers:
- Project scaffolding with Nuxt 3 & TypeScript.
- Supabase project + environment variables.
- Client-side sign-in / magic link / social OAuth.
- Secure SSR session cookies on server routes.
- Global Pinia auth store with auto-refresh.
- Route middleware for protected pages.
- Production deployment to Vercel.
Table of Contents
- Table of Contents
- 1. Prerequisites & Terminology
- 2. Create Nuxt 3 Project & Install Packages
- 3. Add Environment Variables
- 4. Create Supabase Client Composable
- 5. Pinia Auth Store
- 6. Server API Routes for Session Cookies
- 7. Middleware to Protect Routes
- 8. Auto-Refresh on Server Side
- 9. UI Components
- 10. Supabase Remote Config (Row Level Security)
- 11. Deployment to Vercel
- 12. Troubleshooting
- 13. Further Reading & Resources
- 14. Conclusion
1. Prerequisites & Terminology
- Node.js 20.
- Supabase account (free tier).
- Familiarity with Vue 3, Composition API.
Term | Meaning |
---|---|
JWT | JSON Web Token returned by Supabase session |
Refresh token | Token used to renew JWT silently |
Server-side rendering | HTML generated on server for SEO and auth cookies |
2. Create Nuxt 3 Project & Install Packages
npx nuxi init nuxt-supa
cd nuxt-supa
yarn install
yarn add @supabase/supabase-js@2 @vueuse/core pinia
Enable Pinia in nuxt.config.ts
:
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
})
3. Add Environment Variables
.env
(never commit):
SUPABASE_URL=https://xxxx.supabase.co
SUPABASE_ANON_KEY=ey...
Nuxt auto-injects NITRO_PUBLIC_
prefix; we can expose safely:
export default defineNuxtConfig({
runtimeConfig: {
public: {
supabaseUrl: process.env.SUPABASE_URL,
supabaseKey: process.env.SUPABASE_ANON_KEY,
},
},
})
4. Create Supabase Client Composable
composables/useSupabase.ts
:
import { createClient } from '@supabase/supabase-js'
export const useSupabase = () => {
const config = useRuntimeConfig()
const supabase = createClient(config.public.supabaseUrl, config.public.supabaseKey, {
auth: { persistSession: false },
})
return supabase
}
We disable local storage because we’ll store JWT in httpOnly cookie.
5. Pinia Auth Store
stores/auth.ts
:
import { defineStore } from 'pinia'
import { useSupabase } from '@/composables/useSupabase'
export const useAuthStore = defineStore('auth', () => {
const user = ref<null | any>(null)
const supabase = useSupabase()
async function signInEmail(email: string) {
const { error } = await supabase.auth.signInWithOtp({ email })
if (error) throw error
}
async function signInOAuth(provider: 'github' | 'google') {
const { error } = await supabase.auth.signInWithOAuth({
provider,
options: { redirectTo: `${location.origin}/confirm` },
})
if (error) throw error
}
async function signOut() {
await $fetch('/api/auth/logout', { method: 'POST' })
user.value = null
}
return { user, signInEmail, signInOAuth, signOut }
})
6. Server API Routes for Session Cookies
server/api/auth/callback.post.ts
:
import { serverSupabaseClient } from '#supabase/server'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// body contains {access_token, refresh_token, expires_in, token_type, provider_token}
setCookie(event, 'sb-access-token', body.access_token, {
httpOnly: true,
sameSite: 'lax',
maxAge: body.expires_in,
})
setCookie(event, 'sb-refresh-token', body.refresh_token, {
httpOnly: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7,
})
return 'ok'
})
For logout:
export default defineEventHandler((event) => {
deleteCookie(event, 'sb-access-token')
deleteCookie(event, 'sb-refresh-token')
return 'signed out'
})
7. Middleware to Protect Routes
middleware/auth.global.ts
:
export default defineNuxtRouteMiddleware(async (to) => {
if (!to.meta.requiresAuth) return
const { data } = await useFetch('/api/user')
if (!data.value?.id) return navigateTo('/login')
})
Apply in page:
<script setup>
definePageMeta({ requiresAuth: true })
</script>
8. Auto-Refresh on Server Side
server/api/user.get.ts
:
import jwt from 'jsonwebtoken'
import { createClient } from '@supabase/supabase-js'
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const access = getCookie(event, 'sb-access-token')
const refresh = getCookie(event, 'sb-refresh-token')
if (!access || !refresh) return null
try {
jwt.verify(access, '', { algorithms: ['HS256'] })
return jwt.decode(access)
} catch {
// expired – refresh
const supabase = createClient(config.public.supabaseUrl, config.public.supabaseKey)
const { data, error } = await supabase.auth.setSession({
access_token: access,
refresh_token: refresh,
})
if (error) return null
setCookie(event, 'sb-access-token', data.session!.access_token, {
httpOnly: true,
sameSite: 'lax',
maxAge: data.session!.expires_in,
})
return data.user
}
})
9. UI Components
LoginForm.vue
with email input ->auth.signInEmail
.Callback.vue
readscode
&state
from URL, posts to/api/auth/callback
, then redirects.- NavBar shows user avatar & sign-out.
10. Supabase Remote Config (Row Level Security)
Enable RLS on profiles
table:
alter table profiles enable row level security;
create policy "Individuals can view their profile" on profiles for select using (auth.uid() = id);
11. Deployment to Vercel
vercel.json
:
{
"env": {
"SUPABASE_URL": "https://xxxx.supabase.co",
"SUPABASE_ANON_KEY": "..."
}
}
Build command npm run build
, output .output
for Nitro.
12. Troubleshooting
Issue | Fix |
---|---|
AuthApiError: Invalid login | Ensure redirect URL whitelisted in Supabase dashboard |
Cookies missing on SSR | Add domain & secure flags in setCookie during production |
Infinite redirect | Check route middleware & user API response |
13. Further Reading & Resources
- Nuxt Auth best practices doc.
- Supabase Auth helpers for Nuxt (not yet stable in 2025).
- RFC: Server sessions in Nuxt 3.
14. Conclusion
You now have full email, magic link, and social login powered by Supabase, integrated with Nuxt 3’s SSR and Pinia state. Extend with database CRUD, storage uploads, and edge functions to build a robust Vue SaaS! 🚀