Skip to main content

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

name
string
New display name for the user
image
string
New profile image URL

Returns

result
null
Returns null on success

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

roomId
string
required
The room/channel identifier to send the message to
content
string
required
The message text content
userName
string
required
Display name of the sender
sessionId
string
Session identifier for demo mode tracking

Returns

messageId
Id<'messages'>
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

messageId
Id<'messages'>
required
The ID of the message to delete
sessionId
string
Session identifier for demo mode authorization

Returns

result
null
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

roomId
string
required
The room identifier to update presence in
data
object
required
Presence data to store
data.cursor
{x: number, y: number}
Cursor position coordinates
data.position
{x: number, y: number}
Position coordinates (alias for cursor)
data.status
string
User status text
data.userName
string
User’s display name
data.userImage
string
URL to user’s avatar
data.color
string
Color code for cursor/avatar
data.name
string
Alternative name field
sessionId
string
Session identifier for demo mode. Required if not authenticated.

Returns

result
null
Returns null on success

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

roomId
string
required
The room identifier to leave
sessionId
string
Session identifier for demo mode

Returns

result
null
Returns null on success

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

url
string
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

storageId
Id<'_storage'>
required
The storage ID returned after uploading the file
name
string
required
Original filename
type
string
required
MIME type of the file (e.g., “image/png”, “application/pdf”)
size
number
required
File size in bytes
sessionId
string
Session identifier for demo mode tracking

Returns

fileId
Id<'files'>
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

fileId
Id<'files'>
required
The ID of the file to delete
sessionId
string
Session identifier for demo mode authorization

Returns

result
null
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

count
number
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

Build docs developers (and LLMs) love