Skip to main content

Overview

The Utilities API provides essential system functions including secure file upload URL generation and other utility operations needed across the platform. Location: convex/shared/utils.ts

Generate Upload URL

Generates a secure upload URL for file uploads to Convex storage.
const uploadUrl = await convex.mutation(api.shared.utils.generateUploadUrl);

// Use the URL to upload a file
const file = new File(['content'], 'example.jpg', { type: 'image/jpeg' });

const response = await fetch(uploadUrl, {
  method: 'POST',
  body: file
});

const { storageId } = await response.json();
console.log('File uploaded with ID:', storageId);
"https://upload.convex.dev/upload/abc123def456?token=xyz789"

File Upload Process

The file upload process follows these steps:
1

Generate Upload URL

Call generateUploadUrl to get a secure, temporary upload URL
2

Upload File

Use the returned URL to upload your file via HTTP POST
3

Get Storage ID

The upload response contains a storageId that you can use to reference the file
4

Use Storage ID

Use the storageId in other API calls (e.g., creating products with images)

Supported File Types

Images

Formats: JPEG, PNG, WebP, GIF, SVG
Max Size: 10MB
Recommended: WebP for web, PNG for transparency

Documents

Formats: PDF, DOC, DOCX, TXT
Max Size: 25MB
Use Case: Trade licenses, contracts, receipts

Videos

Formats: MP4, WebM, MOV
Max Size: 100MB
Use Case: Product demos, store tours

Audio

Formats: MP3, WAV, OGG
Max Size: 25MB
Use Case: Voice notes, audio descriptions

Complete Upload Example

Here’s a complete example showing how to upload a file and use it:
import { useState } from 'react';
import { useMutation } from 'convex/react';
import { api } from './convex/_generated/api';

function FileUploadComponent() {
  const [uploading, setUploading] = useState(false);
  const [storageId, setStorageId] = useState<string | null>(null);
  
  const generateUploadUrl = useMutation(api.shared.utils.generateUploadUrl);
  const createProduct = useMutation(api.shared.products.createProduct);

  const handleFileUpload = async (file: File) => {
    try {
      setUploading(true);
      
      // Step 1: Generate upload URL
      const uploadUrl = await generateUploadUrl();
      
      // Step 2: Upload file
      const response = await fetch(uploadUrl, {
        method: 'POST',
        body: file
      });
      
      const { storageId } = await response.json();
      setStorageId(storageId);
      
      // Step 3: Use storage ID in product creation
      await createProduct({
        name: "New Product",
        category: "k123456789",
        price: 19.99,
        isActive: true,
        primaryImage: storageId // Use the uploaded file
      });
      
      console.log('Product created with uploaded image!');
      
    } catch (error) {
      console.error('Upload failed:', error);
    } finally {
      setUploading(false);
    }
  };

  return (
    <div>
      <input
        type="file"
        accept="image/*"
        onChange={(e) => {
          const file = e.target.files?.[0];
          if (file) handleFileUpload(file);
        }}
        disabled={uploading}
      />
      {uploading && <p>Uploading...</p>}
      {storageId && <p>Upload successful! ID: {storageId}</p>}
    </div>
  );
}

Security & Best Practices

Always validate file types and sizes on the client side before uploading:
function validateFile(file: File): boolean {
  // Check file size (10MB limit for images)
  if (file.size > 10 * 1024 * 1024) {
    throw new Error('File too large. Maximum size is 10MB.');
  }
  
  // Check file type
  const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
  if (!allowedTypes.includes(file.type)) {
    throw new Error('Invalid file type. Only JPEG, PNG, and WebP are allowed.');
  }
  
  return true;
}
Upload URLs expire after 1 hour for security. Generate a new URL if upload fails:
async function uploadWithRetry(file: File, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const uploadUrl = await generateUploadUrl();
      const response = await fetch(uploadUrl, {
        method: 'POST',
        body: file
      });
      
      if (response.ok) {
        return await response.json();
      }
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      console.warn(`Upload attempt ${i + 1} failed, retrying...`);
    }
  }
}
Track upload progress for better user experience:
async function uploadWithProgress(file: File, onProgress: (progress: number) => void) {
  const uploadUrl = await generateUploadUrl();
  
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    
    xhr.upload.addEventListener('progress', (e) => {
      if (e.lengthComputable) {
        const progress = (e.loaded / e.total) * 100;
        onProgress(progress);
      }
    });
    
    xhr.addEventListener('load', () => {
      if (xhr.status === 200) {
        resolve(JSON.parse(xhr.responseText));
      } else {
        reject(new Error('Upload failed'));
      }
    });
    
    xhr.addEventListener('error', () => reject(new Error('Upload failed')));
    
    xhr.open('POST', uploadUrl);
    xhr.send(file);
  });
}

Error Handling

Status Code: 500
{
  "error": "Failed to generate upload URL. Please try again."
}
Status Code: 400
{
  "error": "File upload failed",
  "details": "File size exceeds maximum limit"
}
Status Code: 400
{
  "error": "Invalid file type",
  "allowedTypes": ["image/jpeg", "image/png", "image/webp"]
}
Upload URLs are temporary and expire after 1 hour. Always generate a fresh URL for each upload operation.
For better performance, compress images before uploading and use WebP format when possible for web applications.