api.auth.getCurrentUserGet the currently authenticated user with their profile data (merges Better Auth data with application user data)
import { useQuery } from "convex/react";
import { api } from "@/convex/_generated/api";
const user = useQuery(api.auth.getCurrentUser);
// Returns: Object with _id, email, name, image, emailVerified, etc.
// undefined if loading, null if not authenticatedauthClient.signIn.email()Sign in with email and password
emailstringUser's email address
passwordstringUser's password
rememberMeboolean?Keep user signed in (optional)
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.signIn.email({
email: "user@example.com",
password: "password123",
rememberMe: true,
});
if (error) {
console.error("Sign in failed:", error.message);
}authClient.signUp.email()Create a new account with email and password
emailstringUser's email address
passwordstringUser's password (min 8 chars)
namestringUser's display name
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.signUp.email({
email: "newuser@example.com",
password: "securePassword123",
name: "John Doe",
});
if (error) {
console.error("Sign up failed:", error.message);
} else {
// User created, may need email verification
}authClient.signIn.social()Sign in with OAuth providers (Google, GitHub, Slack)
provider'google' | 'github' | 'slack'OAuth provider name
callbackURLstring?Redirect URL after auth (optional)
import { authClient } from "@/lib/auth-client";
// Google OAuth
await authClient.signIn.social({
provider: "google",
callbackURL: "/dashboard",
});
// GitHub OAuth
await authClient.signIn.social({
provider: "github",
});authClient.signIn.magicLink()Send a magic link to user's email for passwordless authentication
emailstringUser's email address
import { authClient } from "@/lib/auth-client";
await authClient.signIn.magicLink({
email: "user@example.com",
});
// User will receive an email with a sign-in linkauthClient.twoFactor.enable()Enable two-factor authentication for the current user
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.twoFactor.enable({
password: "currentPassword",
});
if (data) {
// data contains QR code URI and backup codes
console.log("QR Code:", data.qrCodeUri);
console.log("Backup codes:", data.backupCodes);
}authClient.signOut()Sign out the current user and clear their session
import { authClient } from "@/lib/auth-client";
import { useRouter } from "next/navigation";
const router = useRouter();
await authClient.signOut();
router.push("/sign-in");authClient.useSession()React hook to get the current session (client-side)
import { authClient } from "@/lib/auth-client";
function MyComponent() {
const { data: session, isPending } = authClient.useSession();
if (isPending) return <div>Loading...</div>;
if (!session) return <div>Not authenticated</div>;
return <div>Hello {session.user.name}</div>;
}betterAuthComponent.getAuthUser(ctx)Get the authenticated user in a Convex function
import { query } from "./_generated/server";
import { betterAuthComponent } from "./auth";
export const myProtectedQuery = query({
args: {},
handler: async (ctx) => {
const user = await betterAuthComponent.getAuthUser(ctx);
if (!user) {
throw new Error("Not authenticated");
}
// user contains Better Auth user data
return { userId: user.id, email: user.email };
},
});api.auth.getCurrentUser (implementation)Example of merging auth user with application user data
import { query } from "./_generated/server";
import { betterAuthComponent } from "./auth";
export const getCurrentUser = query({
args: {},
handler: async (ctx) => {
const authUser = await betterAuthComponent.getAuthUser(ctx);
if (!authUser) return null;
// Get application user data
const user = await ctx.db
.query("users")
.withIndex("userId", (q) => q.eq("userId", authUser.id))
.first();
if (!user) return null;
// Merge auth metadata with app data
return {
...user,
email: authUser.email,
name: authUser.name,
image: authUser.image,
emailVerified: authUser.emailVerified,
};
},
});Protected Mutation ExampleExample of a mutation that requires authentication
import { mutation } from "./_generated/server";
import { v } from "convex/values";
import { betterAuthComponent } from "./auth";
export const createItem = mutation({
args: {
title: v.string(),
description: v.string(),
},
handler: async (ctx, args) => {
const authUser = await betterAuthComponent.getAuthUser(ctx);
if (!authUser) {
throw new Error("Unauthorized");
}
// Get app user
const user = await ctx.db
.query("users")
.withIndex("userId", (q) => q.eq("userId", authUser.id))
.first();
if (!user) throw new Error("User not found");
// Create item linked to user
const itemId = await ctx.db.insert("items", {
userId: user._id,
title: args.title,
description: args.description,
createdAt: Date.now(),
});
return itemId;
},
});Add your own Convex functions in the convex/ directory:
// convex/myModule.ts
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";
export const myQuery = query({
args: { id: v.id("tableName") },
handler: async (ctx, args) => {
return await ctx.db.get(args.id);
},
});
export const myMutation = mutation({
args: { name: v.string() },
handler: async (ctx, args) => {
return await ctx.db.insert("tableName", {
name: args.name,
});
},
});Then use them in your components:
import { useQuery, useMutation } from "convex/react";
import { api } from "@/convex/_generated/api";
const data = useQuery(api.myModule.myQuery, { id });
const mutate = useMutation(api.myModule.myMutation);
await mutate({ name: "value" });useQuery(api.module.function, args)Subscribe to a Convex query that updates in real-time
functionQueryReferenceThe query function to call
argsobjectArguments to pass to the query
const data = useQuery(api.module.myQuery, { id: "123" });
// data updates automatically when database changes
// Returns undefined while loading, then your data
if (data === undefined) return <div>Loading...</div>;
if (data === null) return <div>Not found</div>;
return <div>{data.name}</div>;useMutation(api.module.function)Get a mutation function to modify data
functionMutationReferenceThe mutation function to call
const mutate = useMutation(api.module.myMutation);
// Use in event handlers
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
try {
const result = await mutate({ name: "value" });
console.log("Created:", result);
} catch (error) {
console.error("Error:", error);
}
};useAction(api.module.function)Get an action function for long-running or external operations
functionActionReferenceThe action function to call
const action = useAction(api.module.myAction);
// Actions can call external APIs, use fetch, etc.
const handleExternalCall = async () => {
const result = await action({
apiKey: "key",
param: "value"
});
console.log("External API result:", result);
};Pagination ExampleImplement paginated queries for large datasets
// convex/items.ts
import { query } from "./_generated/server";
import { v } from "convex/values";
export const listItems = query({
args: {
paginationOpts: paginationOptsValidator,
},
handler: async (ctx, args) => {
return await ctx.db
.query("items")
.order("desc")
.paginate(args.paginationOpts);
},
});
// In component
import { usePaginatedQuery } from "convex/react";
const { results, status, loadMore } = usePaginatedQuery(
api.items.listItems,
{},
{ initialNumItems: 10 }
);
// results is array of items
// status is "CanLoadMore" | "LoadingMore" | "Exhausted"
// loadMore(n) loads n more itemsFile Upload PatternHandle file uploads using Convex storage
// convex/files.ts
import { mutation } from "./_generated/server";
import { v } from "convex/values";
export const generateUploadUrl = mutation({
args: {},
handler: async (ctx) => {
return await ctx.storage.generateUploadUrl();
},
});
export const saveFile = mutation({
args: {
storageId: v.id("_storage"),
fileName: v.string(),
},
handler: async (ctx, args) => {
return await ctx.db.insert("files", {
storageId: args.storageId,
fileName: args.fileName,
uploadedAt: Date.now(),
});
},
});
// In component
const generateUploadUrl = useMutation(api.files.generateUploadUrl);
const saveFile = useMutation(api.files.saveFile);
const handleUpload = async (file: File) => {
// 1. Get upload URL
const uploadUrl = await generateUploadUrl();
// 2. Upload file
const result = await fetch(uploadUrl, {
method: "POST",
body: file,
});
const { storageId } = await result.json();
// 3. Save metadata
await saveFile({ storageId, fileName: file.name });
};External API Call (Action)Call external APIs from Convex actions
// convex/external.ts
import { action } from "./_generated/server";
import { v } from "convex/values";
export const fetchExternalData = action({
args: { userId: v.string() },
handler: async (ctx, args) => {
// Actions can use fetch
const response = await fetch(
`https://api.example.com/users/${args.userId}`
);
const data = await response.json();
// Actions can call mutations
await ctx.runMutation(api.myModule.saveData, {
data: data,
});
return data;
},
});Database IndexesQuery with indexes for better performance
// convex/schema.ts
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
items: defineTable({
userId: v.id("users"),
title: v.string(),
status: v.string(),
createdAt: v.number(),
})
.index("by_user", ["userId"])
.index("by_status", ["status", "createdAt"]),
});
// convex/items.ts - Using indexes
export const getUserItems = query({
args: { userId: v.id("users") },
handler: async (ctx, args) => {
return await ctx.db
.query("items")
.withIndex("by_user", (q) =>
q.eq("userId", args.userId)
)
.collect();
},
});
export const getItemsByStatus = query({
args: { status: v.string() },
handler: async (ctx, args) => {
return await ctx.db
.query("items")
.withIndex("by_status", (q) =>
q.eq("status", args.status)
)
.order("desc") // orders by createdAt
.take(50);
},
});Scheduled Functions (Crons)Run functions on a schedule
// convex/crons.ts
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";
const crons = cronJobs();
// Run daily at midnight UTC
crons.daily(
"clean up old data",
{ hourUTC: 0, minuteUTC: 0 },
internal.cleanup.deleteOldRecords
);
// Run every 5 minutes
crons.interval(
"sync external data",
{ minutes: 5 },
internal.sync.syncData
);
export default crons;
// convex/cleanup.ts
import { internalMutation } from "./_generated/server";
export const deleteOldRecords = internalMutation({
args: {},
handler: async (ctx) => {
const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
const oldRecords = await ctx.db
.query("logs")
.filter((q) => q.lt(q.field("createdAt"), thirtyDaysAgo))
.collect();
for (const record of oldRecords) {
await ctx.db.delete(record._id);
}
},
});Real-time FilteringUse filters for dynamic queries
import { query } from "./_generated/server";
import { v } from "convex/values";
export const searchItems = query({
args: {
searchTerm: v.string(),
status: v.optional(v.string()),
},
handler: async (ctx, args) => {
let results = await ctx.db.query("items").collect();
// Filter by search term
if (args.searchTerm) {
results = results.filter((item) =>
item.title
.toLowerCase()
.includes(args.searchTerm.toLowerCase())
);
}
// Filter by status if provided
if (args.status) {
results = results.filter(
(item) => item.status === args.status
);
}
return results;
},
});Error Handling in QueriesHandle errors gracefully in queries and mutations
// In Convex functions
export const myQuery = query({
args: { id: v.id("items") },
handler: async (ctx, args) => {
const item = await ctx.db.get(args.id);
if (!item) {
throw new Error("Item not found");
}
return item;
},
});
// In components
const MyComponent = () => {
const data = useQuery(api.module.myQuery, { id });
if (data === undefined) {
return <div>Loading...</div>;
}
// Query errors are thrown and can be caught with ErrorBoundary
return <div>{data.name}</div>;
};
// With mutation error handling
const handleSubmit = async () => {
try {
await mutate({ data });
toast.success("Success!");
} catch (error) {
toast.error(error.message || "Failed to save");
}
};TypeScript TypesUse generated types for type safety
import { api } from "@/convex/_generated/api";
import { Id } from "@/convex/_generated/dataModel";
import { Doc } from "@/convex/_generated/dataModel";
// Use Doc for document types
type User = Doc<"users">;
type Todo = Doc<"todos">;
// Use Id for document IDs
const userId: Id<"users"> = "..." as Id<"users">;
// Type-safe function arguments
import { FunctionArgs } from "convex/server";
type CreateItemArgs = FunctionArgs<typeof api.items.create>;
// Type-safe return values
import { FunctionReturnType } from "convex/server";
type UserData = FunctionReturnType<typeof api.auth.getCurrentUser>;Optimistic UpdatesUpdate UI immediately for better UX
const optimisticCreate = useOptimisticMutation(
api.items.create
);
const [optimisticItems, setOptimisticItems] = useState<Item[]>([]);
const items = useQuery(api.items.list) || [];
const allItems = [...items, ...optimisticItems];
const handleCreate = async (newItem: NewItem) => {
const tempId = crypto.randomUUID();
const optimistic = { ...newItem, _id: tempId };
// Add to optimistic state immediately
setOptimisticItems((prev) => [...prev, optimistic]);
try {
await optimisticCreate(newItem);
// Remove from optimistic state on success
setOptimisticItems((prev) =>
prev.filter((item) => item._id !== tempId)
);
} catch (error) {
// Remove and show error
setOptimisticItems((prev) =>
prev.filter((item) => item._id !== tempId)
);
toast.error("Failed to create item");
}
};Best PracticesFollow these patterns for robust applications
// 1. Always validate args with Convex validators
export const myMutation = mutation({
args: {
email: v.string(),
age: v.number(),
tags: v.array(v.string()),
},
handler: async (ctx, args) => { /* ... */ },
});
// 2. Use indexes for efficient queries
// Define in schema.ts:
.index("by_user_and_status", ["userId", "status"])
// 3. Keep queries fast (<1s)
// For heavy computation, use actions instead
// 4. Handle authentication consistently
const authUser = await betterAuthComponent.getAuthUser(ctx);
if (!authUser) throw new Error("Unauthorized");
// 5. Use pagination for large datasets
// Instead of .collect(), use .paginate()
// 6. Avoid N+1 queries
// Batch related queries when possible
// 7. Use proper TypeScript types
// Import from _generated/dataModel
// 8. Test edge cases
// Null checks, empty arrays, missing fields
// 9. Use transactions for related updates
// All mutations are atomic by default
// 10. Monitor function performance
// Check dashboard for slow queries