Vue.js + Convex Backend with Clerk Authentication 🔑 Full-Stack Tutorial
Authentication and backend setup are often the trickiest parts of building a modern web app. If you’ve ever struggled with managing users, sessions, and real-time data in Vue.js, you’re not alone.
In this tutorial, we’ll build a Vue.js application that uses:
- Convex → a reactive backend + database with real-time updates
- Clerk → a complete authentication and user management solution
By the end, you’ll have a working full-stack Vue.js app with secure authentication, real-time user management, and a clean project structure.
🎥 Prefer video? Watch the full tutorial here:
Why Convex + Clerk?
🔹 Convex
Convex is an open-source reactive backend designed for app developers. While it’s often described as a database, that’s selling it short. Convex also provides:
- Server functions
- Real-time updates
- Vector search
- Cron jobs
- File storage
- Automatic type safety
The feature that really stands out is server functions with real-time updates. This makes Convex a great fit for apps that need live data syncing without complex backend setup.
🔹 Clerk
Clerk is a user authentication and management platform. It handles:
- Sign up / Sign in / Sign out
- Session management
- Cookies
- User profile management
It’s easy to integrate, and the free tier is generous enough to get started. Clerk also provides prebuilt UI components like SignInButton and UserButton, which simplify the process of adding authentication to your app.
Demo: What We’re Building
The app we’ll build allows users to:
- Sign up with email (with verification)
- Sign in to an existing account
- View and update their profile
- Sync user data between Clerk and Convex
Convex manages the backend and database, while Clerk handles authentication. Together, they provide a seamless full-stack experience.
Project Setup
- Create a Vue.js app
npm init vue@latest my-app
cd my-app
- Install Convex
npm install convex
npx convex dev
This creates a convex/
directory with schema and function files, and gives you a project URL.
- Install Clerk for Vue
npm install @clerk/vue
-
Configure environment variables
-
VITE_CONVEX_URL
→ fromnpx convex dev
-
VITE_CLERK_PUBLISHABLE_KEY
→ from your Clerk dashboard
-
Convex Schema
We’ll create a simple users table in Convex with an index on clerkId
.
// convex/schema.ts
import { defineSchema, defineTable } from "convex/schema";
export default defineSchema({
users: defineTable({
clerkId: "string",
email: "string",
name: "string",
createdAt: "number",
}).index("by_clerk_id", ["clerkId"]),
});
Convex User Functions
Now that we’ve defined our schema, let’s create the functions that will manage user data.
These functions live in convex/users.ts
and handle:
- Creating a new user when they first sign in with Clerk
- Fetching the current authenticated user
- Updating user profile data (with validation and authorization checks)
// convex/users.ts
import { mutation, query } from "./_generated/server";
/**
* Creates a new user in the database or returns existing user ID.
*/
export const getOrCreateUser = mutation({
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
const existingUser = await ctx.db
.query("users")
.withIndex("by_clerk_id", (q) => q.eq("clerkId", identity.subject))
.first();
if (existingUser) return existingUser._id;
return await ctx.db.insert("users", {
clerkId: identity.subject,
email: identity.email || "",
name: `${identity.givenName || ""} ${identity.familyName || ""}`.trim(),
createdAt: Date.now(),
});
},
});
/**
* Retrieves the current authenticated user's profile information.
*/
export const getCurrentUser = query({
handler: async (ctx) => {
const identity = await ctx.auth.getUserIdentity();
if (!identity) return null;
return await ctx.db
.query("users")
.withIndex("by_clerk_id", (q) => q.eq("clerkId", identity.subject))
.first();
},
});
/**
* Updates user profile data or creates a new user if one doesn't exist.
*/
export const updateUserData = mutation({
handler: async (
ctx,
args: { clerkId: string; email: string; name: string }
) => {
if (!args.clerkId || !args.email) {
throw new Error("Missing required fields: clerkId and email");
}
if (!args.email.includes("@")) {
throw new Error("Invalid email format");
}
const identity = await ctx.auth.getUserIdentity();
if (!identity) throw new Error("Not authenticated");
if (identity.subject !== args.clerkId) {
throw new Error("Unauthorized");
}
const existingUser = await ctx.db
.query("users")
.withIndex("by_clerk_id", (q) => q.eq("clerkId", args.clerkId))
.first();
if (existingUser) {
return await ctx.db.patch(existingUser._id, {
email: args.email,
name: args.name,
});
} else {
return await ctx.db.insert("users", {
clerkId: args.clerkId,
email: args.email,
name: args.name,
createdAt: Date.now(),
});
}
},
});
🔑 Key Points
-
Authentication enforced: Every function checks
ctx.auth.getUserIdentity()
. - Authorization enforced: Users can only update their own data.
- Validation included: Required fields and email format are checked.
-
Upsert behavior:
updateUserData
updates if the user exists, otherwise creates a new record.
ConvexProvider.vue
This component acts as the bridge between Clerk and Convex. It ensures that when a user signs in with Clerk, Convex is also authenticated with the correct JWT, and user data is synced.
<template>
<div v-if="isReady">
<slot />
</div>
<div v-else class="convex-loading">
<LoadingSpinner
size="medium"
color="success"
text="Connecting to database..."
/>
</div>
</template>
<script setup lang="ts">
import { useConvexClient, useConvexMutation } from "convex-vue";
import { useUser, useSession } from "@clerk/vue";
import { ref, watch } from "vue";
import { api } from "../../convex/_generated/api";
import LoadingSpinner from "./LoadingSpinner.vue";
const { user, isLoaded: clerkIsLoaded } = useUser();
const { session, isLoaded: sessionIsLoaded } = useSession();
const isReady = ref(false);
const convex = useConvexClient();
const { mutate: updateUserData } = useConvexMutation(api.users.updateUserData);
const lastSyncedUser = ref<any>(null);
const updateAuth = async () => {
if (!clerkIsLoaded.value || !sessionIsLoaded.value) return;
if (user.value && session.value) {
try {
convex.setAuth(() => session.value!.getToken({ template: "convex" }));
const currentUser = {
clerkId: user.value.id,
email: user.value.primaryEmailAddress?.emailAddress || "",
name: `${user.value.firstName || ""} ${user.value.lastName || ""}`.trim(),
};
if (
!lastSyncedUser.value ||
lastSyncedUser.value.email !== currentUser.email ||
lastSyncedUser.value.name !== currentUser.name
) {
await updateUserData(currentUser);
lastSyncedUser.value = currentUser;
}
} catch (error) {
console.error("Error setting up Convex authentication:", error);
}
} else {
convex.close();
}
isReady.value = true;
};
watch([user, session, clerkIsLoaded, sessionIsLoaded], updateAuth, {
immediate: true,
});
</script>
Wrap Up
We’ve built a Vue.js app with Convex as the backend and Clerk for authentication.
- Convex handles the database, server functions, and real-time updates.
- Clerk manages authentication, sessions, and user profiles.
This stack makes it easy to build full-stack Vue.js apps without writing a custom backend.
🔗 Source Code on GitHub: https://github.com/aaronksaunders/vue-convex-clerk-auth
🎥 Watch the Full Video Tutorial: https://youtu.be/q4orGOuD_mI
FAQ
Q: Can I use Convex with React instead of Vue?
Yes! Convex has official support for React, and most examples are React-based.
Q: Is Clerk free to use?
Yes, Clerk has a free tier that’s great for small projects and testing.
Q: Does Convex support real-time updates?
Yes, Convex is reactive by design — queries automatically update when data changes.
Top comments (0)