Vue.js integration for Convex - the fullstack TypeScript development platform.
- 🚀 SSR Support - Full server-side rendering compatibility with payload hydration
- 🔐 Authentication - Built-in auth support for server-side requests
- ⚡ Reactive Queries - Vue-native composables with reactive data binding
- 🔄 Real-time Updates - Automatic UI updates when data changes
- 📦 TypeScript - Full type safety with Convex schema inference
- 🎯 No Hydration Flickering - Seamless data transfer from server to client
# ✨ Auto-detect
npx nypm install @adinvadim/convex-vue
# npm
npm install @adinvadim/convex-vue
# yarn
yarn add @adinvadim/convex-vue
# pnpm
pnpm install @adinvadim/convex-vue
// main.ts
import { createApp } from "vue";
import { createConvexVue } from "@adinvadim/convex-vue";
import App from "./App.vue";
const app = createApp(App);
app.use(
createConvexVue({
convexUrl: process.env.VITE_CONVEX_URL!,
})
);
app.mount("#app");
<!-- Component.vue -->
<script setup lang="ts">
import { ref } from "vue";
import {
useConvexQuery,
useConvexMutation,
useConvexAction,
} from "@adinvadim/convex-vue";
import { api } from "./convex/_generated/api";
// Query
const { data: messages, isLoading } = useConvexQuery(
api.messages.getMessages,
{}
);
// Mutation
const { mutate: sendMessage, isLoading: isSending } = useConvexMutation(
api.messages.send
);
// Action
const { mutate: generateSummary, isLoading: isGenerating } = useConvexAction(
api.messages.generateSummary
);
const newMessage = ref("");
async function handleSend() {
if (newMessage.value.trim()) {
await sendMessage({ text: newMessage.value });
newMessage.value = "";
}
}
async function handleGenerateSummary() {
await generateSummary({});
}
</script>
<template>
<div v-if="isLoading">Loading...</div>
<div v-else>
<div v-for="message in messages" :key="message._id">
{{ message.text }}
</div>
<form @submit.prevent="handleSend">
<input v-model="newMessage" placeholder="Type a message..." />
<button type="submit" :disabled="isSending">
{{ isSending ? "Sending..." : "Send" }}
</button>
</form>
<button @click="handleGenerateSummary" :disabled="isGenerating">
{{ isGenerating ? "Generating..." : "Generate Summary" }}
</button>
</div>
</template>
For Nuxt applications, create a plugin to integrate Convex with automatic payload injection:
// plugins/convex.ts
import { createConvexVue } from "@adinvadim/convex-vue";
export default defineNuxtPlugin((nuxtApp) => {
const config = useRuntimeConfig();
const ssrConvexState = useState("convex", () => ({}));
if (!config.public.convexUrl) {
console.error(
"Convex URL is not configured. Please add it to your nuxt.config.ts"
);
throw new Error("Missing Convex URL configuration");
}
const ssrConvexState = useState("convex", () => ({}));
const convex = createConvexVue({
convexUrl: config.public.convexUrl,
ssr: {
payloadStorage() {
return ssrConvexState;
},
},
});
nuxtApp.vueApp.use(convex);
});
// plugins/convex.ts
import { createConvexVue } from "@adinvadim/convex-vue";
export default defineNuxtPlugin((nuxtApp) => {
const config = useRuntimeConfig();
const { userId, isLoaded, getToken } = useAuth();
const event = useRequestEvent();
const ssrConvexState = useState("convex", () => ({}));
const convex = createConvexVue({
convexUrl: config.public.convexUrl,
auth: {
isAuthenticated: computed(() => !!userId.value),
isLoading: computed(() => !isLoaded.value),
getToken: async (opts) => {
try {
if (import.meta.server && event) {
return await event.context.auth().getToken({
template: "convex",
});
}
return await getToken.value({
template: "convex",
skipCache: opts.forceRefreshToken,
});
} catch (error) {
return null;
}
},
},
ssr: {
// Use Nuxt's built-in payload system for perfect isolation
payloadStorage() {
return ssrConvexState;
},
},
});
nuxtApp.vueApp.use(convex);
});
Convex Vue includes advanced SSR support with automatic payload hydration to prevent data flickering during client-side hydration.
- Server-side: Queries executed with
useConvexQuery
automatically store their results in an SSR payload - Client-side: During hydration, the same queries retrieve data from the payload instead of showing loading states
- Seamless transition: No flickering or empty states during the server-to-client handoff
Returns the ConvexClient
instance for one-off queries and custom functionality.
import { useConvex } from "@adinvadim/convex-vue";
const convex = useConvex();
const data = await convex.query(api.todos.list, {});
Subscribes to a Convex Query with reactive data binding and SSR support.
const { data, isLoading, error, suspense } = useConvexQuery(
api.todos.list,
{ completed: true }, // reactive arguments
{ enabled: true } // options
);
await suspense(); // for <Suspense /> boundary
Subscribes to a Convex Paginated Query.
const {
data,
lastPage,
isLoading,
isLoadingMore,
isDone,
loadMore,
reset,
pages,
error,
suspense,
} = useConvexPaginatedQuery(
api.todos.list,
{ completed: true },
{ numItems: 50 }
);
Handles Convex Mutations with optimistic updates support.
const {
isLoading,
error,
mutate: addTodo,
} = useConvexMutation(api.todos.add, {
onSuccess() {
todo.value = "";
},
onError(err) {
console.error(err);
},
optimisticUpdate(ctx) {
const current = ctx.getQuery(api.todos.list, {});
if (!current) return;
ctx.setQuery(api.todos.list, {}, [
{
_creationTime: Date.now(),
_id: "optimistic_id" as Id<"todos">,
completed: false,
text: todo.text,
},
...current,
]);
},
});
Handles Convex Actions.
const { isLoading, error, mutate } = useConvexAction(api.some.action, {
onSuccess(result) {
console.log(result);
},
onError(err) {
console.error(err);
},
});
Template component for queries with loading, error, and empty states.
<ConvexQuery :query="api.todos.list" :args="{}">
<template #loading>Loading todos...</template>
<template #error="{ error }">{{ error }}</template>
<template #empty>No todos yet.</template>
<template #default="{ data: todos }">
<ul>
<li v-for="todo in todos" :key="todo._id">
<Todo :todo="todo" />
</li>
</ul>
</template>
</ConvexQuery>
Template component for paginated queries.
<ConvexPaginatedQuery
:query="api.todos.paginatedList"
:args="{}"
:options="{ numItems: 5 }"
>
<template #loading>Loading todos...</template>
<template #error="{ error, reset }">
<p>{{ error }}</p>
<button @click="reset">Retry</button>
</template>
<template #default="{ data: todos, isDone, loadMore, isLoadingMore, reset }">
<ul>
<li v-for="todo in todos" :key="todo._id">
<Todo :todo="todo" />
</li>
</ul>
<Spinner v-if="isLoadingMore" />
<footer>
<button :disabled="isDone" @click="loadMore">Load more</button>
<button @click="reset">Reset</button>
</footer>
</template>
</ConvexPaginatedQuery>
import { createConvexVue } from "@adinvadim/convex-vue";
import { clerkPlugin } from "vue-clerk/plugin";
const app = createApp(App).use(clerkPlugin, {
publishableKey: import.meta.env.VITE_CLERK_PUBLISHABLE_KEY,
});
const authState = {
isLoading: ref(true),
session: ref(undefined),
};
app.config.globalProperties.$clerk.addListener((arg) => {
authState.isLoading.value = false;
authState.session.value = arg.session;
});
const convexVue = createConvexVue({
convexUrl: import.meta.env.VITE_CONVEX_URL,
auth: {
isAuthenticated: computed(() => !!authState.session.value),
isLoading: authState.isLoading,
getToken: async ({ forceRefreshToken }) => {
try {
return await authState.session.value?.getToken({
template: "convex",
skipCache: forceRefreshToken,
});
} catch (error) {
return null;
}
},
},
});
app.use(convexVue);
import { createConvexVue } from "@adinvadim/convex-vue";
import { createAuth0 } from "@auth0/auth0-vue";
const auth = createAuth0({
domain: import.meta.env.VITE_AUTH0_DOMAIN,
clientId: import.meta.env.VITE_AUTH0_CLIENTID,
authorizationParams: {
redirect_uri: window.location.origin,
},
});
const convexVue = createConvexVue({
convexUrl: import.meta.env.VITE_CONVEX_URL,
auth: {
isAuthenticated: auth.isAuthenticated,
isLoading: auth.isLoading,
getToken: async ({ forceRefreshToken }) => {
try {
const response = await auth.getAccessTokenSilently({
detailedResponse: true,
cacheMode: forceRefreshToken ? "off" : "on",
});
return response.id_token;
} catch (error) {
return null;
}
},
installNavigationGuard: true,
needsAuth: (to) => to.meta.needsAuth,
redirectTo: () => ({ name: "Login" }),
},
});
app.use(convexVue);
@adinvadim/convex-vue
- Core Vue.js integration with composables and plugin