# Use SupaHost with Nuxt

> Connect a Nuxt app to a SupaHost project with @supabase/ssr so the session lives in cookies. Runtime config, a browser plugin, a server helper, server routes and email sign-in.

Source: https://supahost.app/docs/getting-started/nuxt · Updated 2026-11-11

Nuxt renders pages on the server and hydrates them in the browser, so both sides need to know who the user is. `@supabase/ssr` stores the Supabase session in cookies, which the browser sends with every request, and gives you a client for each side.

The examples use the `recipes` table from the [quickstart](/docs/getting-started/quickstart).

## Install

```bash
npx nuxi@latest init recipe-box
cd recipe-box
npm install @supabase/supabase-js @supabase/ssr
```

## Add your project keys

Declare the values in runtime config so Nuxt can fill them from the environment:

```ts
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    public: {
      supabaseUrl: '',
      supabaseAnonKey: '',
    },
  },
})
```

Then set them in `.env`, using the Project URL and `anon` key from the **Connect** tab (subtab **App**):

```bash
NUXT_PUBLIC_SUPABASE_URL=https://app-one.acme.supahost.space
NUXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIs...
```

Anything under `public` is sent to the browser. If server code needs the `service_role` key, put it in a non-public key such as `runtimeConfig.supabaseServiceRoleKey` (env `NUXT_SUPABASE_SERVICE_ROLE_KEY`) and only read it in `server/`.

## A client for the browser

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

export default defineNuxtPlugin(() => {
  const config = useRuntimeConfig()
  const supabase = createBrowserClient(
    config.public.supabaseUrl,
    config.public.supabaseAnonKey
  )
  return { provide: { supabase } }
})
```

The `.client.ts` suffix keeps it out of server rendering. Components get it with `const { $supabase } = useNuxtApp()`.

## A client for the server

Server code creates a fresh client per request from that request's cookies:

```ts
// server/utils/supabase.ts
import { createServerClient } from '@supabase/ssr'
import type { H3Event } from 'h3'

export function serverSupabase(event: H3Event) {
  const config = useRuntimeConfig(event)

  return createServerClient(config.public.supabaseUrl, config.public.supabaseAnonKey, {
    cookies: {
      getAll() {
        return Object.entries(parseCookies(event)).map(([name, value]) => ({ name, value }))
      },
      setAll(cookiesToSet) {
        cookiesToSet.forEach(({ name, value, options }) =>
          setCookie(event, name, value, options)
        )
      },
    },
  })
}
```

Files in `server/utils` are auto-imported in server routes, as are `parseCookies` and `setCookie`.

## Read data on the server

```ts
// server/api/recipes.get.ts
export default defineEventHandler(async (event) => {
  const supabase = serverSupabase(event)
  const { data, error } = await supabase
    .from('recipes')
    .select('id, title, minutes')
    .order('created_at', { ascending: false })

  if (error) throw createError({ statusCode: 500, statusMessage: error.message })
  return data
})
```

```vue
<!-- pages/index.vue -->
<script setup lang="ts">
const { data: recipes } = await useFetch('/api/recipes', {
  headers: useRequestHeaders(['cookie']),
})
</script>

<template>
  <ul>
    <li v-for="r in recipes" :key="r.id">{{ r.title }} ({{ r.minutes }} min)</li>
  </ul>
</template>
```

Passing the cookie header matters during server rendering: without it, the API route runs as a signed-out visitor.

## Sign users in

Sign in from the browser client. It writes the session cookies, so the next server request is authenticated too.

```vue
<!-- pages/login.vue -->
<script setup lang="ts">
const { $supabase } = useNuxtApp()
const email = ref('')
const password = ref('')
const message = ref('')

async function signIn() {
  const { error } = await $supabase.auth.signInWithPassword({
    email: email.value,
    password: password.value,
  })
  if (error) message.value = error.message
  else await navigateTo('/')
}

async function signUp() {
  const { error } = await $supabase.auth.signUp({
    email: email.value,
    password: password.value,
    options: { emailRedirectTo: `${window.location.origin}/auth/callback` },
  })
  message.value = error ? error.message : 'Check your inbox to confirm your email.'
}
</script>

<template>
  <form @submit.prevent="signIn">
    <input v-model="email" type="email" required />
    <input v-model="password" type="password" required />
    <button type="submit">Sign in</button>
    <button type="button" @click="signUp">Create account</button>
    <p>{{ message }}</p>
  </form>
</template>
```

The confirmation email returns to `/auth/callback` with a `code`. A server route exchanges it for a session:

```ts
// server/routes/auth/callback.get.ts
export default defineEventHandler(async (event) => {
  const { code } = getQuery(event)
  if (typeof code === 'string') {
    const { error } = await serverSupabase(event).auth.exchangeCodeForSession(code)
    if (!error) return sendRedirect(event, '/')
  }
  return sendRedirect(event, '/login?failed=1')
})
```

To check the user in a server route, call `await serverSupabase(event).auth.getUser()`. It confirms the token with Auth, unlike `getSession()`, which only reads the cookie.

## Allow your redirect URLs

On the project's **Auth** tab, set **Site URL** to your production address and list other origins under **Redirect URLs**, then click **Save Auth settings**:

```text
http://localhost:3000/**
https://recipes.example.com/**
```

## Using the @nuxtjs/supabase module instead

The community module `@nuxtjs/supabase` wraps the same `@supabase/ssr` setup and adds composables such as `useSupabaseClient()`. It works with SupaHost: set `SUPABASE_URL` to your project URL and `SUPABASE_KEY` to the `anon` key. Check its `redirect` option, which by default sends signed-out visitors to a login page.

## Next steps

- [Server-side auth and SSR](/docs/auth/server-side)
- [Social login (OAuth)](/docs/auth/social-login)
- [Generating TypeScript types](/docs/api/generating-types)
- [Deploying your app on Vercel](/docs/integrations/vercel) or [Netlify and Cloudflare](/docs/integrations/netlify-cloudflare)
