@convex-dev/r2

中文文档
0.10.4发布于 2026/10/10Apache-2.0TypeScriptES Module近 30 天下载 64.3万

A R2 component for Convex.

原始使用文档
文档目录17 节
  1. Prerequisites
  2. Cloudflare Account
  3. Convex App
  4. Installation
  5. Using multiple buckets
  6. Uploading files
  7. Using a custom object key
  8. Storing Files from Actions
  9. Serving Files
  10. Generating file URLs in queries
  11. Serving Files from Cloudflare's CDN via Custom Domains
  12. Set up an R2 custom domain
  13. Build a CDN URL from an object key
  14. Deleting Files
  15. Accessing File Metadata
  16. Listing and paginating metadata
  17. Accessing metadata after upload

Convex R2 Component

npm version

Store and serve files with Cloudflare R2.

// or @convex-dev/r2/svelte for Svelte!
import { useUploadFile } from "@convex-dev/r2/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 r2.getUrl(key);
const response = await fetch(url);

Check out the example app for a complete example.

Prerequisites

Cloudflare Account

  • Create a Cloudflare account
  • Create an R2 bucket
  • Set the bucket name as an environment variable R2_BUCKET in your Convex deployment via npx convex env set R2_BUCKET <bucket-name>.
  • 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 four values that you'll need later:
    • Token Value: R2_TOKEN
    • Access Key ID: R2_ACCESS_KEY_ID
    • Secret Access Key: R2_SECRET_ACCESS_KEY
    • Endpoint: R2_ENDPOINT

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/r2

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 r2 from "@convex-dev/r2/convex.config.js";

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

export default app;

Set your API credentials using the values you recorded earlier:

npx convex env set R2_TOKEN xxxxx
npx convex env set R2_ACCESS_KEY_ID xxxxx
npx convex env set R2_SECRET_ACCESS_KEY xxxxx
npx convex env set R2_ENDPOINT xxxxx
npx convex env set R2_BUCKET xxxxx

Note: Environment variables are only needed if you don't pass config directly to the R2 constructor. You can also pass individual values to override specific environment variables.

Using multiple buckets

To use multiple R2 buckets, create separate R2 instances with different config:

import { R2 } from "@convex-dev/r2";
import { components } from "./_generated/api";

// Primary bucket — reads from R2_* environment variables
const primary = new R2(components.r2);

// Archive bucket — configured explicitly
const archive = new R2(components.r2, {
  bucket: process.env.ARCHIVE_BUCKET!,
  endpoint: process.env.ARCHIVE_ENDPOINT!,
  accessKeyId: process.env.ARCHIVE_ACCESS_KEY_ID!,
  secretAccessKey: process.env.ARCHIVE_SECRET_ACCESS_KEY!,
});

Each instance maintains its own config and S3 client. Metadata from all buckets coexists in the same Convex table, indexed by bucket name.

Uploading files

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

  • generates the signed url
  • uploads the file to R2
  • stores the file's metadata in your Convex database

Important: For proper TypeScript typing in your callbacks, pass your DataModel type as a generic parameter to r2.clientApi<DataModel>(). This ensures that ctx parameters are properly typed with your database schema, and queries return Doc<"tableName"> instead of GenericDocument.

  1. Instantiate a R2 component client in a file in your app's convex/ folder:

    // convex/example.ts
    import { R2 } from "@convex-dev/r2";
    import { components } from "./_generated/api";
    import type { DataModel } from "./_generated/dataModel";
    
    export const r2 = new R2(components.r2);
    
    // Pass DataModel as a generic type parameter to get proper TypeScript typing
    // for all callback contexts. Without this, ctx will be typed as GenericDocument
    // instead of your specific table types.
    export const { generateUploadUrl, syncMetadata } = r2.clientApi<DataModel>({
      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/r2/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 r2 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 R2, 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/r2/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 r2.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 r2.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 r2 instance.

// convex/example.ts
import { R2 } from "@convex-dev/r2";
import { components } from "./_generated/api";

export const r2 = new R2(components.r2);

// 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 r2.generateUploadUrl(key);
  },
});

Storing Files from Actions

Files can be stored in R2 directly from actions using the r2.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 { R2 } from "@convex-dev/r2";

const r2 = new R2(components.r2);

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 R2,
    // 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. A
    // Content-Disposition and Cache-Control header can also be provided if
    // necessary.
    const key = await r2.store(ctx, blob, {
      key: "my-custom-key",
      type: "image/jpeg",
      disposition: "attachment; filename=example.jpg",
      cacheControl: "max-age=3600",
    });

    // 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 R2
  • Syncs metadata to your Convex database
  • Returns the key that can be used to access the file later

Serving Files

Files stored in R2 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 r2.getUrl function of the R2 component client.

// convex/listMessages.ts
import { components } from "./_generated/api";
import { query } from "./_generated/server";
import { R2 } from "@convex-dev/r2";

const r2 = new R2(components.r2);

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 r2.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" />;
}

Serving Files from Cloudflare's CDN via Custom Domains

r2.getUrl() generates signed S3 URLs that expire (default 15 minutes) and route directly to R2's S3 API endpoint, bypassing Cloudflare's edge cache. For use cases where you need permanent, CDN-cached URLs (profile pictures, avatars, public assets), you can connect a custom domain to your R2 bucket and build the URL directly from the object key (eg., cdn.example.com, where example.com is a domain you own).

Set up an R2 custom domain

You'll first need to connect a custom domain to your R2 bucket - follow their docs to get started.

Cloudflare caches the most common file types automatically. They provide a complete list as well as info on how to enable caching for less common types in their docs.

Build a CDN URL from an object key

// convex/files.ts
import { R2 } from "@convex-dev/r2";
import { components } from "./_generated/api";
import { query } from "./_generated/server";

const r2 = new R2(components.r2);

// Build CDN URL from a stored object key.
function getCdnUrl(key: string): string {
  // Replace with your domain
  const base = "cdn.example.com"
  return `${base}/${key.split('/').map(encodeURIComponent).join('/')`
}

export const listMessages = query({
  args: {},
  handler: async (ctx) => {
    const messages = await ctx.db.query("messages").collect();
    return messages.map((message) => ({
      ...message,
      // Return cached, CDN served image
      imageUrl: getCdnUrl(message.imageKey),
    }));
  },
});

Note: Don't use the r2.dev public development URL in production - it's rate limited and uncached. Use a custom domain instead.

Deleting Files

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

// convex/images.ts
import { v } from "convex/values";
import { mutation } from "./_generated/server";
import { R2 } from "@convex-dev/r2";

const r2 = new R2(components.r2);

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

Accessing File Metadata

File metadata of an R2 file can be accessed from actions via r2.getMetadata:

// convex/images.ts
import { v } from "convex/values";
import { query } from "./_generated/server";
import { R2 } from "@convex-dev/r2";

const r2 = new R2(components.r2);

export const getMetadata = query({
  args: {
    key: v.string(),
  },
  handler: async (ctx, args) => {
    return await r2.getMetadata(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 or paginated from actions via r2.listMetadata and r2.pageMetadata.

// convex/example.ts
import { query } from "./_generated/server";
import { R2 } from "@convex-dev/r2";

const r2 = new R2(components.r2);

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

export const page = query({
  args: {
    paginationOpts: paginationOptsValidator,
  },
  handler: async (ctx, args) => {
    return r2.pageMetadata(ctx, args.paginationOpts);
  },
});

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 r2.getMetadata can be used to access the metadata of the newly uploaded file.

// convex/example.ts
import { R2, type R2Callbacks } from "@convex-dev/r2";
import { components } from "./_generated/api";
import type { DataModel } from "./_generated/dataModel";

export const r2 = new R2(components.r2);

const callbacks: R2Callbacks = internal.example;

export const { generateUploadUrl, syncMetadata, onSyncMetadata } = r2.clientApi<DataModel>(
  {
    // 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 R2
      // metadata table
      const metadata = await r2.getMetadata(ctx, args.key);
      // log metadata of synced object
      console.log("metadata", metadata);
    },
  },
);

版本与发布记录

@convex-dev/r2 共发布 29 个版本,首次发布于 2025/01/04;过去 12 个月发布了 10 个版本。

版本发布日期距今
0.10.4latest2026/10/101 个月前
0.10.32026/10/071 个月前
0.10.22026/06/164 个月前
0.10.12026/04/236 个月前
0.10.02026/04/176 个月前
0.9.22026/04/146 个月前

关于 @convex-dev/r2 的常见问题

@convex-dev/r2 是什么?

A R2 component for Convex.

如何安装 @convex-dev/r2?

在项目目录运行 npm install @convex-dev/r2,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:convex(^1.25.4)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@convex-dev/r2 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@convex-dev/r2 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@convex-dev/r2 可以商用吗?

@convex-dev/r2 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@convex-dev/r2 的下载量有多少?

根据 npm 官方下载统计,@convex-dev/r2 在 2026/10/11 – 2026/11/09 这 30 天内被下载 642,502 次,日均约 21,417 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@convex-dev/r2 还在维护吗?

最新版本 0.10.4 发布于 2026/10/10(1 个月前);至今共发布 29 个版本,其中过去 12 个月发布了 10 个;首次发布于 2025/01/04。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。