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.
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. If you want server rendering, see Nuxt instead.
Install
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:
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
// 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:
// 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
<!-- 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
<!-- 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:
// 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:
http://localhost:5173/**
https://recipes.example.com/**