Introduction
Getting started with Vue
#Automatic Setup With Create Instant App
The fastest way to get started with Instant with Vue is to use create-instant-app to scaffold a new project with Instant already set up.
To get started run:
npx create-instant-app --vue
#Manual Setup
Create a blank Vue + Vite app:
npm create vue@latest my-app
Add the InstantDB Vue Library:
npm i @instantdb/vue
Use instant-cli to set up a new Instant project. This will prompt you to log in if you haven't already. It will then create a schema file, permissions file, and update your .env file.
npx instant-cli init
Create a database client in src/lib/db.ts:
import { init } from '@instantdb/vue';import schema from '../instant.schema';export const db = init({appId: import.meta.env.VITE_INSTANT_APP_ID!,schema,useDateObjects: true,});
You're now ready to make queries and transactions to your database!
#Creating a To-Do List App
Let's add a "todo" entity to our schema file at src/instant.schema.ts:
import { i } from '@instantdb/vue';const _schema = i.schema({entities: {$files: i.entity({path: i.string().unique().indexed(),url: i.string(),}),$users: i.entity({email: i.string().unique().indexed().optional(),imageURL: i.string().optional(),type: i.string().optional(),}),todos: i.entity({text: i.string(),done: i.boolean(),createdAt: i.date(),}),},links: {$usersLinkedPrimaryUser: {forward: {on: '$users',has: 'one',label: 'linkedPrimaryUser',onDelete: 'cascade',},reverse: {on: '$users',has: 'many',label: 'linkedGuestUsers',},},},rooms: {},});//...
Push the schema:
npx instant-cli push
Replace the content of src/App.vue with the following:
<script setup lang="ts">import { ref } from 'vue';import { id, type InstaQLEntity } from '@instantdb/vue';import { db } from './lib/db';import type { AppSchema } from './instant.schema';type Todo = InstaQLEntity<AppSchema, 'todos'>;const { isLoading, error, data } = db.useQuery({ todos: {} });const text = ref('');function addTodo() {const value = text.value.trim();if (!value) return;db.transact(db.tx.todos[id()].update({text: value,done: false,createdAt: Date.now(),}),);text.value = '';}function toggleDone(todo: Todo) {db.transact(db.tx.todos[todo.id].update({ done: !todo.done }));}function deleteTodo(todo: Todo) {db.transact(db.tx.todos[todo.id].delete());}</script><template><div v-if="isLoading">Loading...</div><div v-else-if="error">Error: {{ error.message }}</div><div v-else><h2>Todos</h2><form @submit.prevent="addTodo"><input v-model="text" placeholder="What needs to be done?" type="text" /></form><div v-for="todo in data?.todos ?? []" :key="todo.id"><input type="checkbox" :checked="todo.done" @change="toggleDone(todo)" /><span :class="{ 'line-through': todo.done }">{{ todo.text }}</span><button @click="deleteTodo(todo)">X</button></div><div>Remaining todos: {{ (data?.todos ?? []).filter((t) => !t.done).length }}</div></div></template>
Go to localhost:5173, and huzzah 🎉 You've got a fully functional todo list running!
#Reactivity
Instant's hooks return an object of refs. This lets you destructure the result without losing reactivity, and refs auto-unwrap when you reference them in your template.
<script setup lang="ts">const { isLoading, data, error } = db.useQuery({ todos: {} });// isLoading.value, data.value, error.value are reactive refs</script><template><p v-if="!isLoading">{{ data?.todos.length }} todos</p></template>
Inside the script you access values via .value; inside the template Vue unwraps the ref automatically, so you can write data?.todos directly.
For hooks that return a single value (useConnectionStatus, useLocalId, useUser), you get a single Ref or ComputedRef:
<script setup lang="ts">const status = db.useConnectionStatus();// status.value is reactive</script><template><p>Connection: {{ status }}</p></template>
#Reactive and conditional queries
The first argument of useQuery accepts a MaybeRefOrGetter<Q | null>. That means you can pass a plain query object, a ref containing a query, a computed, or a getter function. Return null from a getter to skip the query:
<script setup lang="ts">import { db } from './lib/db';const { user } = db.useAuth();// Only query when we have a logged-in userconst { isLoading, data } = db.useQuery(() =>user.value ? { todos: {} } : null,);</script><template><p v-if="!user">Please log in.</p><p v-else-if="isLoading">Loading todos...</p><p v-else>{{ data?.todos.length }} todos</p></template>
Any reactive value read inside the getter automatically re-triggers the query when it changes:
<script setup lang="ts">import { ref } from 'vue';import { db } from './lib/db';const filter = ref<'all' | 'active' | 'done'>('all');const { data } = db.useQuery(() => {if (filter.value === 'all') return { todos: {} };return { todos: { $: { where: { done: filter.value === 'done' } } } };});</script><template><button @click="filter = 'all'">All</button><button @click="filter = 'active'">Active</button><button @click="filter = 'done'">Done</button><p>{{ data?.todos.length }} todos</p></template>
#Writing data
Transactions in Vue work the same way they do in React via db.transact:
<script setup lang="ts">import { id } from '@instantdb/vue';import { db } from './lib/db';function addTodo(text: string) {db.transact(db.tx.todos[id()].update({ text, done: false, createdAt: Date.now() }),);}function toggleDone(todo: { id: string; done: boolean }) {db.transact(db.tx.todos[todo.id].update({ done: !todo.done }));}function deleteTodo(todoId: string) {db.transact(db.tx.todos[todoId].delete());}</script>
To learn more see our writing data docs.
#Auth
The Vue SDK supports all of Instant's auth methods: magic codes, guest auth, Google OAuth, and more.
#useAuth
Use db.useAuth() to get the current auth state. This gives you full control over loading, error, and user states:
<script setup lang="ts">import { db } from './lib/db';const { isLoading, error, user } = db.useAuth();</script><template><div v-if="isLoading">Loading...</div><div v-else-if="error">Error: {{ error.message }}</div><div v-else-if="user"><p>Hello, {{ user.isGuest ? 'Guest' : user.email }}!</p><button @click="db.auth.signOut()">Sign out</button></div><div v-else><p>Please log in.</p><button @click="db.auth.signInAsGuest()">Try as guest</button></div></template>
#SignedIn / SignedOut
For simpler cases where you just need to gate content on auth state, use the SignedIn and SignedOut guard components instead:
<script setup lang="ts">import { SignedIn, SignedOut } from '@instantdb/vue';import { db } from './lib/db';</script><template><SignedIn :db="db"><p>You are logged in!</p><button @click="db.auth.signOut()">Sign out</button></SignedIn><SignedOut :db="db"><p>Please log in.</p></SignedOut></template>
useAuth is better when you need access to isLoading, error, or user.isGuest. The guard components are simpler when you just need to show or hide content based on login state.
#Components
#Cursors
A multiplayer cursor component that tracks mouse positions via presence. Wrap any area where you want to show live cursors from other users:
<script setup lang="ts">import { Cursors } from '@instantdb/vue';import { db } from './lib/db';const room = db.room('main', 'my-room-id');</script><template><Cursors :room="room" userCursorColor="tomato"><div>Move your mouse around!</div></Cursors></template>
The Cursors component supports custom cursor rendering via a scoped cursor slot, a configurable wrapper element (as), and inherits class/style from the parent. See the Presence, Cursors, and Activity docs for more details.
#Nuxt
The Vue SDK works with Nuxt. Because Instant is a client-only library (it relies on browser APIs like WebSocket and IndexedDB), you'll want to either wrap Instant-using components with <ClientOnly> or disable SSR for the relevant routes via routeRules in nuxt.config.ts:
export default defineNuxtConfig({routeRules: {'/my-route/**': { ssr: false },},});
Server-side rendering with Instant is not yet supported (let us know if you want this!).