To use Supabase with Vue.js, install @supabase/supabase-js, create a client instance with your project URL and anon key, and wrap it in a Vue composable for reuse across components. You can then perform CRUD operations, listen for auth state changes, and subscribe to real-time updates using the Composition API. Store your keys in environment variables prefixed with VITE_ for Vite-based projects.
Integrating Supabase with Vue.js Using the Composition API
Supabase pairs naturally with Vue.js because both emphasize simplicity and developer experience. This tutorial shows you how to set up the Supabase JavaScript client in a Vue 3 project, create a composable that any component can import, and build a working CRUD interface with authentication. You will also learn how to manage environment variables in a Vite-based Vue project.
Prerequisites
- A Supabase project with at least one table created
- A Vue 3 project scaffolded with Vite (npm create vue@latest)
- Node.js 18+ installed
- Your Supabase project URL and anon key from the Dashboard (Settings > API)
Step-by-step guide
Install the Supabase JavaScript client
Install the Supabase JavaScript client
Add the Supabase JS client library to your Vue project. This is the same package used with React, Svelte, or any JavaScript framework. It provides methods for auth, database queries, storage, and real-time subscriptions.
1npm install @supabase/supabase-jsExpected result: @supabase/supabase-js is added to your package.json dependencies.
Configure environment variables for Vite
Configure environment variables for Vite
Create a .env.local file in your project root and add your Supabase URL and anon key. In Vite-based projects, environment variables must be prefixed with VITE_ to be accessible in client-side code. The anon key is safe to use in the browser because it respects Row Level Security policies. Never expose your service role key in frontend code.
1# .env.local2VITE_SUPABASE_URL=https://your-project.supabase.co3VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Expected result: Environment variables are available via import.meta.env.VITE_SUPABASE_URL in your Vue code.
Create the Supabase client instance
Create the Supabase client instance
Create a dedicated file to initialize the Supabase client. This file exports a single client instance that is reused across your entire app. Placing it in src/lib/ keeps it organized and importable from any component or composable.
1// src/lib/supabase.ts2import { createClient } from '@supabase/supabase-js'34const supabaseUrl = import.meta.env.VITE_SUPABASE_URL5const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY67export const supabase = createClient(supabaseUrl, supabaseAnonKey)Expected result: A single Supabase client instance is exported and ready to use throughout the app.
Build a Vue composable for data fetching
Build a Vue composable for data fetching
Create a composable that wraps Supabase queries with Vue's reactive refs. This pattern follows Vue 3 best practices and makes your data fetching logic reusable. The composable returns reactive data, loading state, and an error ref that your template can bind to directly.
1// src/composables/useTodos.ts2import { ref, onMounted } from 'vue'3import { supabase } from '@/lib/supabase'45export function useTodos() {6 const todos = ref<any[]>([])7 const loading = ref(true)8 const error = ref<string | null>(null)910 async function fetchTodos() {11 loading.value = true12 const { data, error: err } = await supabase13 .from('todos')14 .select('*')15 .order('created_at', { ascending: false })1617 if (err) {18 error.value = err.message19 } else {20 todos.value = data ?? []21 }22 loading.value = false23 }2425 async function addTodo(task: string) {26 const { error: err } = await supabase27 .from('todos')28 .insert({ task, is_complete: false })2930 if (err) {31 error.value = err.message32 } else {33 await fetchTodos()34 }35 }3637 onMounted(fetchTodos)3839 return { todos, loading, error, fetchTodos, addTodo }40}Expected result: The composable returns reactive data that updates the template automatically when queries complete.
Use the composable in a Vue component
Use the composable in a Vue component
Import the composable into a Vue component and bind the reactive refs to your template. The composable handles all the Supabase logic, keeping your component clean and focused on presentation. Use v-for to render the list and v-model for the input.
1<script setup lang="ts">2import { ref } from 'vue'3import { useTodos } from '@/composables/useTodos'45const { todos, loading, error, addTodo } = useTodos()6const newTask = ref('')78async function handleSubmit() {9 if (newTask.value.trim()) {10 await addTodo(newTask.value.trim())11 newTask.value = ''12 }13}14</script>1516<template>17 <div>18 <form @submit.prevent="handleSubmit">19 <input v-model="newTask" placeholder="Add a task..." />20 <button type="submit">Add</button>21 </form>22 <p v-if="loading">Loading...</p>23 <p v-if="error" class="error">{{ error }}</p>24 <ul>25 <li v-for="todo in todos" :key="todo.id">{{ todo.task }}</li>26 </ul>27 </div>28</template>Expected result: The component renders a list of todos fetched from Supabase and allows adding new items.
Add authentication with onAuthStateChange
Add authentication with onAuthStateChange
Listen for auth state changes in your App.vue or a dedicated auth composable. The onAuthStateChange listener fires whenever the user logs in, logs out, or their token refreshes. Use this to update a reactive user ref that your components can check for conditional rendering.
1// src/composables/useAuth.ts2import { ref, onMounted, onUnmounted } from 'vue'3import { supabase } from '@/lib/supabase'4import type { User } from '@supabase/supabase-js'56export function useAuth() {7 const user = ref<User | null>(null)8 let subscription: { unsubscribe: () => void } | null = null910 onMounted(() => {11 supabase.auth.getUser().then(({ data }) => {12 user.value = data.user13 })1415 const { data } = supabase.auth.onAuthStateChange((event, session) => {16 user.value = session?.user ?? null17 })18 subscription = data.subscription19 })2021 onUnmounted(() => {22 subscription?.unsubscribe()23 })2425 return { user }26}Expected result: The user ref updates reactively whenever the authentication state changes.
Complete working example
1// src/lib/supabase.ts2import { createClient } from '@supabase/supabase-js'34const supabaseUrl = import.meta.env.VITE_SUPABASE_URL5const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY67export const supabase = createClient(supabaseUrl, supabaseAnonKey)89// src/composables/useTodos.ts10import { ref, onMounted } from 'vue'11import { supabase } from '@/lib/supabase'1213export function useTodos() {14 const todos = ref<any[]>([])15 const loading = ref(true)16 const error = ref<string | null>(null)1718 async function fetchTodos() {19 loading.value = true20 const { data, error: err } = await supabase21 .from('todos')22 .select('*')23 .order('created_at', { ascending: false })24 if (err) {25 error.value = err.message26 } else {27 todos.value = data ?? []28 }29 loading.value = false30 }3132 async function addTodo(task: string) {33 // RLS must allow INSERT for authenticated users34 const { error: err } = await supabase35 .from('todos')36 .insert({ task, is_complete: false })37 if (err) {38 error.value = err.message39 } else {40 await fetchTodos()41 }42 }4344 async function toggleTodo(id: number, isComplete: boolean) {45 // RLS must allow UPDATE for the record owner46 const { error: err } = await supabase47 .from('todos')48 .update({ is_complete: !isComplete })49 .eq('id', id)50 if (err) {51 error.value = err.message52 } else {53 await fetchTodos()54 }55 }5657 async function deleteTodo(id: number) {58 // RLS must allow DELETE for the record owner59 const { error: err } = await supabase60 .from('todos')61 .delete()62 .eq('id', id)63 if (err) {64 error.value = err.message65 } else {66 await fetchTodos()67 }68 }6970 onMounted(fetchTodos)7172 return { todos, loading, error, fetchTodos, addTodo, toggleTodo, deleteTodo }73}Common mistakes when using Supabase with Vue.js
Why it's a problem: Creating a new Supabase client inside each component instead of sharing a single instance
How to avoid: Create the client once in a dedicated file (e.g., src/lib/supabase.ts) and import it everywhere. Multiple client instances break session management.
Why it's a problem: Forgetting the VITE_ prefix on environment variables, causing them to be undefined at runtime
How to avoid: In Vite projects, only variables prefixed with VITE_ are exposed to client code. Use VITE_SUPABASE_URL, not SUPABASE_URL.
Why it's a problem: Not setting up RLS policies, resulting in empty query results with no error message
How to avoid: When RLS is enabled with no policies, all queries silently return empty arrays. Create appropriate SELECT, INSERT, UPDATE, and DELETE policies for your tables.
Best practices
- Initialize the Supabase client once in a shared module and export it for use across all composables and components
- Use Vue composables to encapsulate Supabase logic, keeping components focused on presentation
- Always check for errors in Supabase responses — destructure { data, error } and handle both cases
- Store Supabase URL and anon key in VITE_ prefixed environment variables, never hardcode them
- Unsubscribe from onAuthStateChange and real-time listeners in onUnmounted to prevent memory leaks
- Use TypeScript with Supabase generated types (supabase gen types typescript) for full type safety in your composables
- Set up RLS policies for every table that your frontend accesses — the anon key is public and must be protected by RLS
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a Vue 3 project using Vite and I want to connect it to Supabase. Show me how to create a reusable composable that fetches data from a 'products' table, handles loading and error states with reactive refs, and includes an addProduct function that inserts a new row. Include the Supabase client setup and environment variable configuration.
Create a Vue 3 composable that connects to my Supabase project and provides CRUD operations for a 'tasks' table. Use the Composition API with ref and onMounted. Include error handling and a real-time subscription that updates the task list when another user adds a task.
Frequently asked questions
Can I use Supabase with Vue 2?
Yes, the @supabase/supabase-js package works with Vue 2, but you cannot use the Composition API composable pattern. Instead, create Supabase methods in your component's methods object and store data in the data() function.
Do I need to use Vuex or Pinia with Supabase?
Not necessarily. For most apps, composables with reactive refs are sufficient. Use Pinia if you need to share Supabase data across many unrelated components or if your app state is complex enough to benefit from a centralized store.
How do I handle real-time updates in Vue?
Use supabase.channel().on('postgres_changes', ...).subscribe() inside a composable with onMounted. When a change arrives, update the reactive ref. Remember to call supabase.removeChannel() in onUnmounted.
Is the anon key safe to use in browser code?
Yes. The anon key is designed for client-side use. It maps to the anon Postgres role, which is restricted by Row Level Security policies. Without proper RLS policies, no data is accessible even with the key.
Why does my query return an empty array instead of an error?
This is how RLS works in Supabase. When RLS is enabled but no policy grants access, queries return empty arrays silently. Check that you have a SELECT policy for the authenticated or anon role on your table.
Can I use Supabase with Vue.js and TypeScript?
Yes. Run supabase gen types typescript --local to generate types from your database schema, then pass them as a generic to createClient for fully typed queries and responses.
Can RapidDev help integrate Supabase into my Vue.js project?
Yes. RapidDev can set up your Supabase integration including auth flows, real-time features, RLS policies, and component architecture tailored to your Vue.js application.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation