Bright ideas and techniques for building with Convex.
Profile image
Indy Khare
2 months ago

Uploading files from React Native or Expo

Uploading files from React Native / Expo

Say you’re building a mobile React Native app that let’s users take voice notes. You’ve built a cool UI that let’s you record audio by following the guide from Expo. The API to record files gives you a URI to the file on disk. (If you’re building a photo app and need image uploads, the ImagePicker API also gives you a URI to your images on disk)

You’ve already setup the Convex mutation to generate an upload URL and the mutation to store the app specific metadata. (Jump to the bottom to see the mutations used in this example)

Now that you have the URI to the file on disk and the mutations to store it on the backend, how do you actually upload the file from the computer to the Convex remote servers?

Here’s a snippet that makes this work in your React Native app. Follow along in the comments to understand each step:

// Setup Convex mutations for use in your React component
const generateUploadUrl = useMutation(api.recordings.generateUploadUrl);
const saveRecording = useMutation(api.recordings.saveRecording);

// Method called to actually upload the file
async function uploadAndSaveRecording(uri: string) {
		// 1. Get upload url from Convex.
		//    This is the first mutation from the Convex docs for uploading.
    const postUrl = await generateUploadUrl();

		// 2. Get the file from the URI pointing to a local file
    const fileData = await fetch(uri);
		// Basic error handling to ensure the file is valid.
    if (!fileData.ok) {
      console.error("Error loading file.", fileData);

		// 3. Get the file contents to upload
    const blob = await fileData.blob();

		// 4. Set up error handling for your upload
    try {
	    // 5. Actually send the file contents to Convex
      const result = await fetch(postUrl, {
        method: "POST",
        // Note: Use the right mime type.
				//       iOS and Android use mp4 for audio recordings.
        headers: { "Content-Type": "audio/mp4" },
        body: blob,
      if (!result.ok) {
				// Note: You may actually want to inform the user of this.
        console.log("Failed to upload.");

			// 6. Once successfully uploaded get the storageId
      const { storageId } = await result.json();

			// 7. Store custom metadata associated with this recording in Convex
      const uploadResult = await saveRecording({ storageId, author: });

    } catch (err) {
			// Note: You may actually want to inform the user of this.
      console.error("Failed to upload.", err);
      throw err;

That’s it! To confirm everything is working, check the file uploads section of your Convex Dashboard.

And check the data table to make sure the application specific metadata is saved.

If it’s all there, then it’s working! Great work!

For completeness here are are the Convex mutations that match the upload code above.

import { v } from "convex/values";
import { mutation } from "./_generated/server";
import { internal } from "./_generated/api";

// Generate the URL that you will actually upload the file to
export const generateUploadUrl = mutation(async (ctx) => {
  return await;

// Mutation to save custom metadata for the file
export const saveRecording = mutation({
  args: { storageId:"_storage"), author: v.string() },
  handler: async (ctx, args) => {
    const recId = await ctx.db.insert("recordings", {
      storageId: args.storageId,
      format: "audio",
    ctx.scheduler.runAfter(0, internal.transcript.transcribe, { recId });
    return recId;
Build in minutes, scale forever.

Convex is the backend application platform with everything you need to build your project. Cloud functions, a database, file storage, scheduling, search, and realtime updates fit together seamlessly.

Get started