npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@atherlabs/convex-object-storage

v0.8.0

Published

An object storage component for Convex supporting R2 and S3.

Readme

Convex Object Storage Component

npm version

Store and serve files with Cloudflare R2 or AWS S3.

// Works with both R2 and S3!
import { useUploadFile } from "@convex-dev/object-storage/react";

// Upload files from React
const uploadFile = useUploadFile(api.example);
// ...in a callback
const key = await uploadFile(file);

// Access files on the server
const url = await storage.getUrl(key);
const response = await fetch(url);

Check out the example app for a complete example.

Prerequisites

Choose your provider and follow the setup instructions:

Option A: Cloudflare R2

  • Create a Cloudflare account
  • Create an R2 bucket
  • Add a CORS policy to the bucket allowing GET and PUT requests from your Convex app. You can also use '*' to allow all origins (use with caution).
    [
      {
        "AllowedOrigins": ["http://localhost:5173"],
        "AllowedMethods": ["GET", "PUT"],
        "AllowedHeaders": ["Content-Type"]
      }
    ]
  • Create an API token
    • On the main R2 page in your Cloudflare dashboard, click Manage R2 API Tokens
    • Click Create API Token
    • Edit the token name
    • Set permissions to Object Read & Write
    • Under Specify bucket, select the bucket you created above
    • Optionally change TTL
    • Click Create API Token
  • On the next screen you'll be provided with values you'll need for environment variables

Option B: AWS S3

Convex App

You'll need a Convex App to use the component. Follow any of the Convex quickstarts to set one up.

Installation

Install the component package:

npm install @convex-dev/object-storage

Create a convex.config.ts file in your app's convex/ folder and install the component by calling use:

// convex/convex.config.ts
import { defineApp } from "convex/server";
import objectStorage from "@convex-dev/object-storage/convex.config";

const app = defineApp();
app.use(objectStorage);

export default app;

Configuration

Set your credentials using environment variables. The component supports multiple prefixes:

For Cloudflare R2:

npx convex env set R2_BUCKET your-bucket-name
npx convex env set R2_ENDPOINT https://your-account-id.r2.cloudflarestorage.com
npx convex env set R2_ACCESS_KEY_ID your-access-key-id
npx convex env set R2_SECRET_ACCESS_KEY your-secret-access-key

For AWS S3:

npx convex env set S3_BUCKET your-bucket-name
npx convex env set S3_REGION us-east-1
npx convex env set S3_ACCESS_KEY_ID your-access-key-id
npx convex env set S3_SECRET_ACCESS_KEY your-secret-access-key

The component auto-detects the provider based on your environment variables, or you can specify it explicitly in code.

Uploading files

File uploads to object storage typically use signed urls. The component provides hooks for React and Svelte that handle the entire upload process:

  • generates the signed url
  • uploads the file to your storage provider (R2 or S3)
  • stores the file's metadata in your Convex database
  1. Instantiate an object storage client in a file in your app's convex/ folder:

    // convex/example.ts
    import { ObjectStorage } from "@convex-dev/object-storage";
    import { components } from "./_generated/api";
    
    export const storage = new ObjectStorage(components.objectStorage);
    
    export const { generateUploadUrl, syncMetadata } = storage.clientApi({
      checkUpload: async (ctx, bucket) => {
        // const user = await userFromAuth(ctx);
        // ...validate that the user can upload to this bucket
      },
     onUpload: async (ctx, bucket, key) => {
       // ...do something with the key
       // This technically runs in the `syncMetadata` mutation, as the upload
       // is performed from the client side. Will run if using the `useUploadFile`
       // hook, or if `syncMetadata` function is called directly. Runs after the
       // `checkUpload` callback.
      },
    });
  2. Use the useUploadFile hook in your component to upload files:

    React:

    // src/App.tsx
    import { FormEvent, useRef, useState } from "react";
    import { useAction } from "convex/react";
    import { api } from "../convex/_generated/api";
    import { useUploadFile } from "@convex-dev/object-storage/react";
    
    export default function App() {
      // Passing the entire api exported from `convex/example.ts` to the hook.
      // This must include `generateUploadUrl` and `syncMetadata` from the storage client api.
      const uploadFile = useUploadFile(api.example);
      const imageInput = useRef<HTMLInputElement>(null);
      const [selectedImage, setSelectedImage] = useState<File | null>(null);
    
      async function handleUpload(event: FormEvent) {
        event.preventDefault();
    
        // The file is uploaded to object storage, metadata is synced to the database, and the
        // key of the newly created object is returned.
        await uploadFile(selectedImage!);
        setSelectedImage(null);
        imageInput.current!.value = "";
      }
      return (
        <form onSubmit={handleUpload}>
          <input
            type="file"
            accept="image/*"
            ref={imageInput}
            onChange={(event) => setSelectedImage(event.target.files![0])}
            disabled={selectedImage !== null}
          />
          <input
            type="submit"
            value="Upload"
            disabled={selectedImage === null}
          />
        </form>
      );
    }

    Svelte:

    <script lang="ts">
       import { useUploadFile } from "@convex-dev/object-storage/svelte";
       import { api } from "../convex/_generated/api";
    
       const uploadFile = useUploadFile(api.example);
    
       let selectedImage = $state<File | null>(null);
    
       async function handleUpload(file: File) {
         await uploadFile(file);
         selectedImage = null;
       }
     </script>
    
     <form
       onsubmit={() => {
         if (selectedImage) handleUpload(selectedImage);
       }}
     >
       <input
         type="file"
         accept="image/*"
         onchange={(e) => {
           selectedImage = e.currentTarget.files?.[0] ?? null;
         }}
         disabled={selectedImage !== null}
       />
       <button type="submit" disabled={selectedImage === null}> Upload </button>
     </form>

Using a custom object key

The storage.generateUploadUrl function generates a uuid to use as the object key by default, but a custom key can be provided if desired. Note: the generateUploadUrl function returned by storage.clientApi does not accept a custom key, as that function is a mutation to be called from the client side and you don't want your client defining your object keys. Providing a custom key requires making your own mutation that calls the generateUploadUrl method of the storage instance.

// convex/example.ts
import { ObjectStorage } from "@convex-dev/object-storage";
import { components } from "./_generated/api";

export const storage = new ObjectStorage(components.objectStorage);

// A custom mutation that creates a key from the user id and a uuid. If the key
// already exists, the mutation will fail.
export const generateUploadUrlWithCustomKey = mutation({
  args: {},
  handler: async (ctx) => {
    // Replace this with whatever function you use to get the current user
    const currentUser = await getUser(ctx);
    if (!currentUser) {
      throw new Error("User not found");
    }
    const key = `${currentUser.id}.${crypto.randomUUID()}`;
    return storage.generateUploadUrl(key);
  },
});

Storing Files from Actions

Files can be stored in object storage directly from actions using the storage.store method. This is useful when you need to store files that are generated or downloaded on the server side.

// convex/example.ts
import { internalAction } from "./_generated/server";
import { ObjectStorage } from "@convex-dev/object-storage";

const storage = new ObjectStorage(components.objectStorage);

export const store = internalAction({
  handler: async (ctx) => {
    // Download a random image from picsum.photos
    const url = "https://picsum.photos/200/300";
    const response = await fetch(url);
    const blob = await response.blob();

    // This function call is the only required part, it uploads the blob to object storage,
    // syncs the metadata, and returns the key. The key is a uuid by default, but
    // an optional custom key can be provided in the options object. A MIME type
    // can also be provided, which will override the type inferred for blobs.
    const key = await storage.store(ctx, blob, {
      key: "my-custom-key",
      type: "image/jpeg",
    });

    // Example use case, associate the key with a record in your database
    await ctx.runMutation(internal.example.insertImage, { key });
  },
});

The store method:

  • Takes a Blob, Buffer, or Uint8Array and stores it in object storage
  • Syncs metadata to your Convex database
  • Returns the key that can be used to access the file later

Serving Files

Files stored in object storage can be served to your users by generating a URL pointing to a given file.

Generating file URLs in queries

The simplest way to serve files is to return URLs along with other data required by your app from queries and mutations.

A file URL can be generated from a object key by the storage.getUrl function of the object storage component client.

// convex/listMessages.ts
import { components } from "./_generated/api";
import { query } from "./_generated/server";
import { ObjectStorage } from "@convex-dev/object-storage";

const storage = new ObjectStorage(components.objectStorage);

export const list = query({
  args: {},
  handler: async (ctx) => {
    // In this example, messages have an imageKey field with the object key
    const messages = await ctx.db.query("messages").collect();
    return Promise.all(
      messages.map(async (message) => ({
        ...message,
        imageUrl: await storage.getUrl(
          message.imageKey,
          // Options object is optional, can be omitted
          {
            // Custom expiration time in seconds, default is 900 (15 minutes)
            expiresIn: 60 * 60 * 24, // 1 day
          }
        ),
      }))
    );
  },
});

File URLs can be used in img elements to render images:

// src/App.tsx
function Image({ message }: { message: { url: string } }) {
  return <img src={message.url} height="300px" width="auto" />;
}

Deleting Files

Files stored in object storage can be deleted from actions or mutations via the storage.deleteObject function, which accepts an object key.

// convex/images.ts
import { v } from "convex/values";
import { mutation } from "./_generated/server";
import { ObjectStorage } from "@convex-dev/object-storage";

const storage = new ObjectStorage(components.objectStorage);

export const deleteObject = mutation({
  args: {
    key: v.string(),
  },
  handler: async (ctx, args) => {
    return await storage.deleteObject(ctx, args.key);
  },
});

Accessing File Metadata

File metadata can be accessed from actions via storage.getMetadata:

// convex/images.ts
import { v } from "convex/values";
import { query } from "./_generated/server";
import { ObjectStorage } from "@convex-dev/object-storage";

const storage = new ObjectStorage(components.objectStorage);

export const getMetadata = query({
  args: {
    key: v.string(),
  },
  handler: async (ctx, args) => {
    return await storage.getMetadata(ctx, args.key);
  },
});

This is an example of the returned document:

{
  "ContentType": "image/jpeg",
  "ContentLength": 125338,
  "LastModified": "2024-03-20T12:34:56Z"
}

The returned document has the following fields:

  • ContentType: the ContentType of the file if it was provided on upload
  • ContentLength: the size of the file in bytes
  • LastModified: the last modified date of the file

Listing and paginating metadata

Metadata can be listed from actions via storage.listMetadata.

// convex/example.ts
import { query } from "./_generated/server";
import { ObjectStorage } from "@convex-dev/object-storage";

const storage = new ObjectStorage(components.objectStorage);

export const list = query({
  args: {
    limit: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    return storage.listMetadata(ctx, args.limit);
  },
});

Accessing metadata after upload

The onSyncMetadata callback can be used to run a mutation after every metadata sync. The useUploadFile hook syncs metadata after every upload, so this function will run each time as well.

Because this runs after metadata sync, the storage.getMetadata can be used to access the metadata of the newly uploaded file.

// convex/example.ts
import { ObjectStorage, type ObjectStorageCallbacks } from "@convex-dev/object-storage";
import { components } from "./_generated/api";

export const storage = new ObjectStorage(components.objectStorage);

const callbacks: ObjectStorageCallbacks = internal.example;

export const { generateUploadUrl, syncMetadata, onSyncMetadata } = storage.clientApi(
  {
    // Pass the functions from this file back into the component.
    // Technically only an object with `onSyncMetadata` is required, the recommended
    // pattern is just for convenience.
    callbacks,

    onSyncMetadata: async (ctx, args) => {
      // args: { bucket: string; key: string; isNew: boolean }
      // args.isNew is true if the key did not previously exist in your Convex
      // metadata table
      const metadata = await storage.getMetadata(ctx, args.key);
      // log metadata of synced object
      console.log("metadata", metadata);
    },
  }
);