# Use SupaHost with Vue

> Connect a Vue 3 app built with Vite to a SupaHost project. Install supabase-js, add env vars, share the session through a composable, query a table and sign users in.

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

A Vue 3 app talks to your SupaHost project straight from the browser with supabase-js and the public `anon` key. This guide uses Vite, the Composition API and the `recipes` table from the [quickstart](/docs/getting-started/quickstart). If you want server rendering, see [Nuxt](/docs/getting-started/nuxt) instead.

## Install

```bash
npm create vue@latest recipe-box
cd recipe-box
npm install
npm install @supabase/supabase-js
```

## Add your project keys

Copy the Project URL and `anon` key from the **Connect** tab (subtab **App**) into `.env.local`:

```bash
VITE_SUPABASE_URL=https://app-one.acme.supahost.space
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIs...
```

`VITE_` variables are built into the browser bundle. That's fine for the `anon` key, which is meant to be public, and never fine for `service_role`.

## Create the client

```ts
// src/lib/supabase.ts
import { createClient } from '@supabase/supabase-js'

export const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_ANON_KEY
)
```

## Share the session with a composable

Keep the session in a module-level `ref` so every component sees the same value:

```ts
// src/composables/useAuth.ts
import { ref } from 'vue'
import type { Session } from '@supabase/supabase-js'
import { supabase } from '@/lib/supabase'

const session = ref<Session | null>(null)

supabase.auth.getSession().then(({ data }) => {
  session.value = data.session
})

supabase.auth.onAuthStateChange((_event, newSession) => {
  session.value = newSession
})

export function useAuth() {
  return {
    session,
    signIn: (email: string, password: string) =>
      supabase.auth.signInWithPassword({ email, password }),
    signUp: (email: string, password: string) =>
      supabase.auth.signUp({
        email,
        password,
        options: { emailRedirectTo: window.location.origin },
      }),
    signOut: () => supabase.auth.signOut(),
  }
}
```

## Query and insert

```vue
<!-- src/components/RecipeList.vue -->
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { supabase } from '@/lib/supabase'
import { useAuth } from '@/composables/useAuth'

type Recipe = { id: number; title: string; minutes: number | null }

const { session } = useAuth()
const recipes = ref<Recipe[]>([])
const newTitle = ref('')
const errorText = ref('')

async function load() {
  const { data, error } = await supabase
    .from('recipes')
    .select('id, title, minutes')
    .order('created_at', { ascending: false })
  if (error) errorText.value = error.message
  else recipes.value = data
}

async function add() {
  const { error } = await supabase.from('recipes').insert({ title: newTitle.value })
  if (error) errorText.value = error.message
  else {
    newTitle.value = ''
    await load()
  }
}

onMounted(load)
</script>

<template>
  <form v-if="session" @submit.prevent="add">
    <input v-model="newTitle" placeholder="New recipe" required />
    <button>Add</button>
  </form>
  <p v-if="errorText">{{ errorText }}</p>
  <ul>
    <li v-for="r in recipes" :key="r.id">{{ r.title }}</li>
  </ul>
</template>
```

Reading works for everyone through the quickstart's select policy. Inserting needs a signed-in user, because the insert policy targets `authenticated` and checks that `user_id` is theirs.

## Add sign-in

```vue
<!-- src/components/SignIn.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { useAuth } from '@/composables/useAuth'

const { session, signIn, signUp, signOut } = useAuth()
const email = ref('')
const password = ref('')
const message = ref('')

async function onSignIn() {
  const { error } = await signIn(email.value, password.value)
  message.value = error ? error.message : ''
}

async function onSignUp() {
  const { error } = await signUp(email.value, password.value)
  message.value = error ? error.message : 'Check your inbox to confirm your email.'
}
</script>

<template>
  <div v-if="session">
    Signed in as {{ session.user.email }}
    <button @click="signOut">Sign out</button>
  </div>
  <form v-else @submit.prevent="onSignIn">
    <input v-model="email" type="email" required />
    <input v-model="password" type="password" required />
    <button type="submit">Sign in</button>
    <button type="button" @click="onSignUp">Create account</button>
    <p>{{ message }}</p>
  </form>
</template>
```

## Protect routes

With Vue Router, a global guard can send signed-out visitors to the sign-in page:

```ts
// src/router/index.ts (excerpt)
import { supabase } from '@/lib/supabase'

router.beforeEach(async (to) => {
  if (!to.meta.requiresAuth) return true
  const { data } = await supabase.auth.getSession()
  return data.session ? true : { name: 'sign-in' }
})
```

This only controls what the UI shows. The real protection is Row Level Security, which applies to every request no matter what the browser does.

## Allow your redirect URLs

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

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

## Next steps

- [Social login (OAuth)](/docs/auth/social-login)
- [Postgres Changes](/docs/realtime/postgres-changes) for live updates
- [Storage access control](/docs/storage/access-control)
- [Querying and filtering data](/docs/api/querying)
