--- name: convex-auth description: Convex Auth - authentication, user management, protected functions, and session handling. Use when working with getAuthUserId, login, logout, authTables, @convex-dev/auth, loggedInUser, currentUser, sessions, or user permissions in Convex applications. license: MIT metadata: author: convex-community version: "1.0" --- # Convex Auth Server Guidelines ## Getting the Authenticated User ID When writing Convex handlers, use the `getAuthUserId` function to get the logged in user's ID. You can then pass this to `ctx.db.get` in queries or mutations to get the user's data. **IMPORTANT:** You can only use this within the `convex/` directory. ```typescript // convex/users.ts import { getAuthUserId } from "@convex-dev/auth/server"; import { query } from "./_generated/server"; export const currentLoggedInUser = query({ args: {}, returns: v.union(v.null(), v.object({ _id: v.id("users"), name: v.optional(v.string()), email: v.optional(v.string()), image: v.optional(v.string()), })), handler: async (ctx) => { const userId = await getAuthUserId(ctx); if (!userId) { return null; } const user = await ctx.db.get(userId); if (!user) { return null; } console.log("User", user.name, user.image, user.email); return user; } }); ``` --- # Logged In User Query If you want to get the current logged in user's data on the frontend, use this function defined in `convex/auth.ts`: ```typescript // convex/auth.ts import { getAuthUserId } from "@convex-dev/auth/server"; import { query } from "./_generated/server"; export const loggedInUser = query({ args: {}, handler: async (ctx) => { const userId = await getAuthUserId(ctx); if (!userId) { return null; } const user = await ctx.db.get(userId); if (!user) { return null; } return user; }, }); ``` Then use the `loggedInUser` query in your React component: ```tsx // src/App.tsx import { useQuery } from "convex/react"; import { api } from "../convex/_generated/api"; function App() { const user = useQuery(api.auth.loggedInUser); if (user === undefined) { return
Loading...
; } if (user === null) { return
Not logged in
; } return
Welcome, {user.name}!
; } ``` --- # Users Table Schema The "users" table within `authTables` has this schema: ```typescript const users = defineTable({ name: v.optional(v.string()), image: v.optional(v.string()), email: v.optional(v.string()), emailVerificationTime: v.optional(v.number()), phone: v.optional(v.string()), phoneVerificationTime: v.optional(v.number()), isAnonymous: v.optional(v.boolean()), }) .index("email", ["email"]) .index("phone", ["phone"]); ``` --- # Schema with Auth Tables When defining your schema with authentication, always spread `authTables`: ```typescript // convex/schema.ts import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; import { authTables } from "@convex-dev/auth/server"; const applicationTables = { // Your application tables here posts: defineTable({ authorId: v.id("users"), title: v.string(), content: v.string(), }).index("by_author", ["authorId"]), }; export default defineSchema({ ...authTables, ...applicationTables, }); ``` --- # Protected Mutations and Queries ## Pattern for Protected Functions Create a helper function to get the logged-in user and throw if not authenticated: ```typescript // convex/utils.ts import { getAuthUserId } from "@convex-dev/auth/server"; import { QueryCtx, MutationCtx } from "./_generated/server"; import { Doc } from "./_generated/dataModel"; export async function getLoggedInUser(ctx: QueryCtx | MutationCtx): Promise> { const userId = await getAuthUserId(ctx); if (!userId) { throw new Error("Not authenticated"); } const user = await ctx.db.get(userId); if (!user) { throw new Error("User not found"); } return user; } export async function getLoggedInUserOrNull(ctx: QueryCtx | MutationCtx): Promise | null> { const userId = await getAuthUserId(ctx); if (!userId) { return null; } return await ctx.db.get(userId); } ``` ## Using the Helper ```typescript // convex/posts.ts import { mutation, query } from "./_generated/server"; import { v } from "convex/values"; import { getLoggedInUser } from "./utils"; export const createPost = mutation({ args: { title: v.string(), content: v.string(), }, returns: v.id("posts"), handler: async (ctx, args) => { const user = await getLoggedInUser(ctx); return await ctx.db.insert("posts", { authorId: user._id, title: args.title, content: args.content, }); }, }); export const myPosts = query({ args: {}, handler: async (ctx) => { const user = await getLoggedInUser(ctx); return await ctx.db .query("posts") .withIndex("by_author", (q) => q.eq("authorId", user._id)) .collect(); }, }); ``` --- # Auth in Scheduled Jobs **CRITICAL:** Auth state does NOT propagate to scheduled jobs. `getAuthUserId()` and `ctx.getUserIdentity()` will ALWAYS return `null` from within a scheduled job. ## Solution: Pass User ID Explicitly ```typescript // convex/tasks.ts import { mutation, internalMutation } from "./_generated/server"; import { internal } from "./_generated/api"; import { v } from "convex/values"; import { getAuthUserId } from "@convex-dev/auth/server"; export const scheduleTask = mutation({ args: { taskData: v.string() }, returns: v.null(), handler: async (ctx, args) => { const userId = await getAuthUserId(ctx); if (!userId) { throw new Error("Not authenticated"); } // Pass the userId to the scheduled function await ctx.scheduler.runAfter(0, internal.tasks.processTask, { userId, taskData: args.taskData, }); return null; }, }); export const processTask = internalMutation({ args: { userId: v.id("users"), taskData: v.string(), }, returns: v.null(), handler: async (ctx, args) => { // Use the passed userId instead of getAuthUserId const user = await ctx.db.get(args.userId); if (!user) { throw new Error("User not found"); } // Process the task with user context console.log(`Processing task for user: ${user.name}`); return null; }, }); ``` --- # HTTP Endpoints with Auth ## Auth Handler Setup The auth handler should be in `convex/http.ts`: ```typescript // convex/http.ts import { httpRouter } from "convex/server"; import { auth } from "./auth"; const http = httpRouter(); auth.addHttpRoutes(http); export default http; ``` ## Custom HTTP Endpoints (in convex/router.ts) Define new HTTP endpoints in a separate file to avoid modifying the auth handler: ```typescript // convex/router.ts import { httpRouter } from "convex/server"; import { httpAction } from "./_generated/server"; const router = httpRouter(); router.route({ path: "/api/webhook", method: "POST", handler: httpAction(async (ctx, req) => { // Handle webhook const body = await req.json(); return new Response(JSON.stringify({ received: true }), { status: 200, headers: { "Content-Type": "application/json" }, }); }), }); export default router; ``` --- # Anonymous Users Always make sure your UIs work well with anonymous users: ```tsx // src/components/UserProfile.tsx import { useQuery } from "convex/react"; import { api } from "../../convex/_generated/api"; export function UserProfile() { const user = useQuery(api.auth.loggedInUser); // Loading state if (user === undefined) { return
Loading...
; } // Anonymous / not logged in if (user === null) { return (

Welcome, Guest!

); } // Logged in user return (
{user.image && {user.name}

Welcome, {user.name || user.email || "User"}!

); } ``` --- # Extending the Users Table If you need to add more fields to the users table, you can extend it in your schema: ```typescript // convex/schema.ts import { defineSchema, defineTable } from "convex/server"; import { v } from "convex/values"; import { authTables } from "@convex-dev/auth/server"; // Extend the users table with additional fields const extendedAuthTables = { ...authTables, users: defineTable({ // Original auth fields name: v.optional(v.string()), image: v.optional(v.string()), email: v.optional(v.string()), emailVerificationTime: v.optional(v.number()), phone: v.optional(v.string()), phoneVerificationTime: v.optional(v.number()), isAnonymous: v.optional(v.boolean()), // Your custom fields role: v.optional(v.union(v.literal("admin"), v.literal("user"))), bio: v.optional(v.string()), preferences: v.optional(v.object({ theme: v.union(v.literal("light"), v.literal("dark")), notifications: v.boolean(), })), }) .index("email", ["email"]) .index("phone", ["phone"]) .index("by_role", ["role"]), }; export default defineSchema({ ...extendedAuthTables, // Your other tables }); ``` --- # Best Practices ## 1. Always Check Authentication First ```typescript export const sensitiveQuery = query({ args: {}, handler: async (ctx) => { const userId = await getAuthUserId(ctx); if (!userId) { throw new Error("Authentication required"); } // Continue with authenticated logic }, }); ``` ## 2. Use Internal Functions for Privileged Operations ```typescript // Public mutation that checks auth export const deleteAccount = mutation({ args: {}, returns: v.null(), handler: async (ctx) => { const userId = await getAuthUserId(ctx); if (!userId) { throw new Error("Not authenticated"); } // Call internal function for the actual deletion await ctx.runMutation(internal.users.deleteUserData, { userId }); return null; }, }); // Internal mutation that doesn't check auth (already verified) export const deleteUserData = internalMutation({ args: { userId: v.id("users") }, returns: v.null(), handler: async (ctx, args) => { // Delete user's data const posts = await ctx.db .query("posts") .withIndex("by_author", (q) => q.eq("authorId", args.userId)) .collect(); for (const post of posts) { await ctx.db.delete(post._id); } return null; }, }); ``` ## 3. Handle Loading States Properly ```tsx function ProtectedComponent() { const user = useQuery(api.auth.loggedInUser); // IMPORTANT: Check for undefined (loading) before null (not authenticated) if (user === undefined) { return ; } if (user === null) { return ; } return ; } ```