Skip to main content
RapidDev - Software Development Agency
supabase-tutorial

How to Use Supabase with Vue.js

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.

What you'll learn

  • How to initialize the Supabase client in a Vue 3 project
  • How to create a reusable composable for Supabase operations
  • How to perform CRUD operations from Vue components
  • How to handle authentication state with Vue reactivity
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner8 min read15-20 minSupabase (all plans), @supabase/supabase-js v2+, Vue 3 with Composition APIMarch 2026RapidDev Engineering Team
TL;DR

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

1

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.

typescript
1npm install @supabase/supabase-js

Expected result: @supabase/supabase-js is added to your package.json dependencies.

2

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.

typescript
1# .env.local
2VITE_SUPABASE_URL=https://your-project.supabase.co
3VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Expected result: Environment variables are available via import.meta.env.VITE_SUPABASE_URL in your Vue code.

3

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.

typescript
1// src/lib/supabase.ts
2import { createClient } from '@supabase/supabase-js'
3
4const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
5const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
6
7export const supabase = createClient(supabaseUrl, supabaseAnonKey)

Expected result: A single Supabase client instance is exported and ready to use throughout the app.

4

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.

typescript
1// src/composables/useTodos.ts
2import { ref, onMounted } from 'vue'
3import { supabase } from '@/lib/supabase'
4
5export function useTodos() {
6 const todos = ref<any[]>([])
7 const loading = ref(true)
8 const error = ref<string | null>(null)
9
10 async function fetchTodos() {
11 loading.value = true
12 const { data, error: err } = await supabase
13 .from('todos')
14 .select('*')
15 .order('created_at', { ascending: false })
16
17 if (err) {
18 error.value = err.message
19 } else {
20 todos.value = data ?? []
21 }
22 loading.value = false
23 }
24
25 async function addTodo(task: string) {
26 const { error: err } = await supabase
27 .from('todos')
28 .insert({ task, is_complete: false })
29
30 if (err) {
31 error.value = err.message
32 } else {
33 await fetchTodos()
34 }
35 }
36
37 onMounted(fetchTodos)
38
39 return { todos, loading, error, fetchTodos, addTodo }
40}

Expected result: The composable returns reactive data that updates the template automatically when queries complete.

5

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.

typescript
1<script setup lang="ts">
2import { ref } from 'vue'
3import { useTodos } from '@/composables/useTodos'
4
5const { todos, loading, error, addTodo } = useTodos()
6const newTask = ref('')
7
8async function handleSubmit() {
9 if (newTask.value.trim()) {
10 await addTodo(newTask.value.trim())
11 newTask.value = ''
12 }
13}
14</script>
15
16<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.

6

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.

typescript
1// src/composables/useAuth.ts
2import { ref, onMounted, onUnmounted } from 'vue'
3import { supabase } from '@/lib/supabase'
4import type { User } from '@supabase/supabase-js'
5
6export function useAuth() {
7 const user = ref<User | null>(null)
8 let subscription: { unsubscribe: () => void } | null = null
9
10 onMounted(() => {
11 supabase.auth.getUser().then(({ data }) => {
12 user.value = data.user
13 })
14
15 const { data } = supabase.auth.onAuthStateChange((event, session) => {
16 user.value = session?.user ?? null
17 })
18 subscription = data.subscription
19 })
20
21 onUnmounted(() => {
22 subscription?.unsubscribe()
23 })
24
25 return { user }
26}

Expected result: The user ref updates reactively whenever the authentication state changes.

Complete working example

src/lib/supabase.ts + src/composables/useTodos.ts
1// src/lib/supabase.ts
2import { createClient } from '@supabase/supabase-js'
3
4const supabaseUrl = import.meta.env.VITE_SUPABASE_URL
5const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY
6
7export const supabase = createClient(supabaseUrl, supabaseAnonKey)
8
9// src/composables/useTodos.ts
10import { ref, onMounted } from 'vue'
11import { supabase } from '@/lib/supabase'
12
13export function useTodos() {
14 const todos = ref<any[]>([])
15 const loading = ref(true)
16 const error = ref<string | null>(null)
17
18 async function fetchTodos() {
19 loading.value = true
20 const { data, error: err } = await supabase
21 .from('todos')
22 .select('*')
23 .order('created_at', { ascending: false })
24 if (err) {
25 error.value = err.message
26 } else {
27 todos.value = data ?? []
28 }
29 loading.value = false
30 }
31
32 async function addTodo(task: string) {
33 // RLS must allow INSERT for authenticated users
34 const { error: err } = await supabase
35 .from('todos')
36 .insert({ task, is_complete: false })
37 if (err) {
38 error.value = err.message
39 } else {
40 await fetchTodos()
41 }
42 }
43
44 async function toggleTodo(id: number, isComplete: boolean) {
45 // RLS must allow UPDATE for the record owner
46 const { error: err } = await supabase
47 .from('todos')
48 .update({ is_complete: !isComplete })
49 .eq('id', id)
50 if (err) {
51 error.value = err.message
52 } else {
53 await fetchTodos()
54 }
55 }
56
57 async function deleteTodo(id: number) {
58 // RLS must allow DELETE for the record owner
59 const { error: err } = await supabase
60 .from('todos')
61 .delete()
62 .eq('id', id)
63 if (err) {
64 error.value = err.message
65 } else {
66 await fetchTodos()
67 }
68 }
69
70 onMounted(fetchTodos)
71
72 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.

ChatGPT Prompt

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.

Supabase Prompt

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.