Overview
Mutations are write functions that modify data in the Convex database. They support both authenticated and demo modes.
Users
updateProfile
Update the current user’s profile information.
Location: convex/users.ts:52
export const updateProfile = mutation({
args: {
name: v.optional(v.string()),
image: v.optional(v.string()),
},
returns: v.null(),
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (!userId) {
throw new ConvexError({
code: "UNAUTHORIZED",
message: "You must be logged in to update your profile",
});
}
const updates: Record<string, string> = {};
if (args.name !== undefined) updates.name = args.name;
if (args.image !== undefined) updates.image = args.image;
if (Object.keys(updates).length > 0) {
await ctx.db.patch(userId, updates);
}
return null;
},
});
Arguments
New display name for the user
Returns
Errors
- Throws
UNAUTHORIZED error if user is not authenticated
Example
await ctx.runMutation(api.users.updateProfile, {
name: "Alice Johnson",
image: "https://example.com/avatar.jpg"
});
Messages
send
Send a new message to a chat room.
Location: convex/messages.ts:27
export const send = mutation({
args: {
roomId: v.string(),
content: v.string(),
userName: v.string(),
sessionId: v.optional(v.string()),
},
returns: v.id("messages"),
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
return await ctx.db.insert("messages", {
roomId: args.roomId,
userId: userId ?? undefined,
content: args.content,
userName: args.userName,
sessionId: args.sessionId,
});
},
});
Arguments
The room/channel identifier to send the message to
Display name of the sender
Session identifier for demo mode tracking
Returns
The ID of the newly created message
Example
const messageId = await ctx.runMutation(api.messages.send, {
roomId: "general",
content: "Hello, world!",
userName: "Alice",
sessionId: "session_xyz"
});
Notes
- Works with or without authentication
- If authenticated,
userId is automatically set
- In demo mode, relies on
sessionId for tracking
remove
Delete a message from a chat room.
Location: convex/messages.ts:49
export const remove = mutation({
args: {
messageId: v.id("messages"),
sessionId: v.optional(v.string()),
},
returns: v.null(),
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
const message = await ctx.db.get(args.messageId);
if (!message) {
return null; // Idempotent
}
const canDelete =
!message.userId ||
message.userId === userId ||
(message.sessionId && message.sessionId === args.sessionId);
if (canDelete) {
await ctx.db.delete(args.messageId);
}
return null;
},
});
Arguments
The ID of the message to delete
Session identifier for demo mode authorization
Returns
Returns null on success or if message already deleted
Authorization
Message can be deleted if any of the following conditions are met:
- Message has no userId (orphaned)
- Current user owns the message
- SessionId matches the message’s sessionId
Example
await ctx.runMutation(api.messages.remove, {
messageId: "jh7abc123...",
sessionId: "session_xyz"
});
Presence
update
Update or create presence data for the current user/session in a room.
Location: convex/presence.ts:44
export const update = mutation({
args: {
roomId: v.string(),
data: presenceDataValidator,
sessionId: v.optional(v.string()),
},
returns: v.null(),
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
if (!userId && !args.sessionId) {
return null;
}
// Prioritize sessionId for lookups
let existing = null;
if (args.sessionId) {
existing = await ctx.db
.query("presence")
.withIndex("by_session_and_room", (q) =>
q.eq("sessionId", args.sessionId).eq("roomId", args.roomId),
)
.first();
} else if (userId) {
existing = await ctx.db
.query("presence")
.withIndex("by_user_and_room", (q) =>
q.eq("userId", userId).eq("roomId", args.roomId),
)
.first();
}
if (existing) {
await ctx.db.patch(existing._id, {
data: args.data,
lastSeen: Date.now(),
});
} else {
await ctx.db.insert("presence", {
roomId: args.roomId,
userId: userId ?? undefined,
sessionId: args.sessionId,
data: args.data,
lastSeen: Date.now(),
});
}
return null;
},
});
Arguments
The room identifier to update presence in
Presence data to storeCursor position coordinates
Position coordinates (alias for cursor)
Color code for cursor/avatar
Session identifier for demo mode. Required if not authenticated.
Returns
Example
await ctx.runMutation(api.presence.update, {
roomId: "canvas-1",
data: {
cursor: { x: 150, y: 200 },
userName: "Alice",
color: "#FF6B6B",
status: "active"
},
sessionId: "session_xyz"
});
Notes
- Prioritizes
sessionId over userId for lookups (supports multiple tabs)
- Updates existing entry or creates new one
- Automatically updates
lastSeen timestamp
- Returns null silently if neither userId nor sessionId is provided
leave
Remove presence entry when user leaves a room.
Location: convex/presence.ts:97
export const leave = mutation({
args: {
roomId: v.string(),
sessionId: v.optional(v.string()),
},
returns: v.null(),
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
let existing = null;
if (args.sessionId) {
existing = await ctx.db
.query("presence")
.withIndex("by_session_and_room", (q) =>
q.eq("sessionId", args.sessionId).eq("roomId", args.roomId),
)
.first();
} else if (userId) {
existing = await ctx.db
.query("presence")
.withIndex("by_user_and_room", (q) =>
q.eq("userId", userId).eq("roomId", args.roomId),
)
.first();
}
if (existing) {
await ctx.db.delete(existing._id);
}
return null;
},
});
Arguments
The room identifier to leave
Session identifier for demo mode
Returns
Example
await ctx.runMutation(api.presence.leave, {
roomId: "canvas-1",
sessionId: "session_xyz"
});
Notes
- Prioritizes
sessionId over userId for lookups
- Idempotent - succeeds even if presence doesn’t exist
- More efficient than updating with stale timestamp
Files
generateUploadUrl
Generate a temporary URL for uploading files to Convex storage.
Location: convex/files.ts:17
export const generateUploadUrl = mutation({
args: {},
returns: v.string(),
handler: async (ctx) => {
return await ctx.storage.generateUploadUrl();
},
});
Arguments
No arguments required.
Returns
Temporary upload URL that expires after a short time
Example
const uploadUrl = await ctx.runMutation(api.files.generateUploadUrl, {});
const response = await fetch(uploadUrl, {
method: "POST",
body: fileData,
});
const { storageId } = await response.json();
Notes
- Works in demo mode (no authentication required)
- URL expires after a short period
- Use the returned URL to POST file data
saveFile
Save file metadata after uploading to storage.
Location: convex/files.ts:25
export const saveFile = mutation({
args: {
storageId: v.id("_storage"),
name: v.string(),
type: v.string(),
size: v.number(),
sessionId: v.optional(v.string()),
},
returns: v.id("files"),
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
return await ctx.db.insert("files", {
storageId: args.storageId,
userId: userId ?? undefined,
sessionId: args.sessionId,
name: args.name,
type: args.type,
size: args.size,
});
},
});
Arguments
The storage ID returned after uploading the file
MIME type of the file (e.g., “image/png”, “application/pdf”)
Session identifier for demo mode tracking
Returns
The ID of the newly created file record
Example
const fileId = await ctx.runMutation(api.files.saveFile, {
storageId: "kg2abc123...",
name: "document.pdf",
type: "application/pdf",
size: 2048576,
sessionId: "session_xyz"
});
Notes
- Call this after successfully uploading file to storage URL
- Works with or without authentication
- Associates file with user if authenticated, otherwise with sessionId
remove
Delete a file from both storage and database.
Location: convex/files.ts:94
export const remove = mutation({
args: {
fileId: v.id("files"),
sessionId: v.optional(v.string()),
},
returns: v.null(),
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);
const file = await ctx.db.get(args.fileId);
if (!file) {
return null; // Idempotent
}
const canDelete =
!file.userId ||
file.userId === userId ||
(file.sessionId && file.sessionId === args.sessionId);
if (canDelete) {
await ctx.storage.delete(file.storageId);
await ctx.db.delete(args.fileId);
}
return null;
},
});
Arguments
The ID of the file to delete
Session identifier for demo mode authorization
Returns
Returns null on success or if file already deleted
Authorization
File can be deleted if any of the following conditions are met:
- File has no userId (orphaned)
- Current user owns the file
- SessionId matches the file’s sessionId
Example
await ctx.runMutation(api.files.remove, {
fileId: "jd9abc123...",
sessionId: "session_xyz"
});
Notes
- Deletes both the storage blob and database record
- Idempotent - succeeds even if file doesn’t exist
- Silently fails if user doesn’t have permission
Internal Mutations
presence.cleanup
Remove stale presence entries. Only callable from cron jobs or internal functions.
Location: convex/presence.ts:133
export const cleanup = internalMutation({
args: {},
returns: v.number(),
handler: async (ctx) => {
const now = Date.now();
const cutoff = now - PRESENCE_TIMEOUT; // 30 seconds
const stalePresence = await ctx.db
.query("presence")
.withIndex("by_last_seen", (q) => q.lt("lastSeen", cutoff))
.collect();
for (const p of stalePresence) {
await ctx.db.delete(p._id);
}
return stalePresence.length;
},
});
Arguments
No arguments required.
Returns
Number of stale presence entries deleted
Example
// In convex/crons.ts
import { cronJobs } from "convex/server";
import { internal } from "./_generated/api";
const crons = cronJobs();
crons.interval(
"cleanup stale presence",
{ minutes: 1 },
internal.presence.cleanup
);
export default crons;
Notes
- Only callable from internal contexts (cron jobs, actions, other internal mutations)
- Removes entries not updated in the last 30 seconds
- Returns count of deleted entries
- Deletes sequentially to avoid write conflicts