---
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 (
);
}
// Logged in user
return (
{user.image &&

}
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 ;
}
```