DEV Community

Aaron K Saunders
Aaron K Saunders

Posted on

Vue.js + Convex Backend with Clerk Authentication 🔑 Full-Stack Tutorial

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

  1. Create a Vue.js app
   npm init vue@latest my-app
   cd my-app
Enter fullscreen mode Exit fullscreen mode
  1. Install Convex
   npm install convex
   npx convex dev
Enter fullscreen mode Exit fullscreen mode

This creates a convex/ directory with schema and function files, and gives you a project URL.

  1. Install Clerk for Vue
   npm install @clerk/vue
Enter fullscreen mode Exit fullscreen mode
  1. Configure environment variables
    • VITE_CONVEX_URL → from npx 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"]),
});
Enter fullscreen mode Exit fullscreen mode

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(),
      });
    }
  },
});
Enter fullscreen mode Exit fullscreen mode

🔑 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>
Enter fullscreen mode Exit fullscreen mode

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)