npm install @convex-dev/r2
Store and serve files with Cloudflare R2.
// 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.
R2_BUCKET
in your Convex
deploymentjson
[
{
"AllowedOrigins": ["http://localhost:5173"],
"AllowedMethods": ["GET", "PUT"],
"AllowedHeaders": ["Content-Type"]
}
]
R2_TOKEN
R2_ACCESS_KEY_ID
R2_SECRET_ACCESS_KEY
R2_ENDPOINT
You'll need a Convex App to use the component. Follow any of the Convex quickstarts to set one up.
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";
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
File uploads to R2 typically use signed urls. The R2 component provides a React hook that handles the entire upload processs:
convex/
folder:
```ts
// convex/example.ts
import { R2 } from "@convex-dev/r2";
import { components } from "./_generated/api";export const r2 = new R2(components.r2);
export const { generateUploadUrl, syncMetadata } = r2.clientApi({
checkUpload: async (ctx, bucket) => {
// const user = await userFromAuth(ctx);
// ...validate that the user can upload to this bucket
},
onUpload: async (ctx, key) => {
// ...do something with the key
// Runs in the syncMetadata
mutation, as the upload is performed from the
// client side. Convenient way to create relations between the newly created
// object key and other data in your Convex database. Runs after the checkUpload
// callback.
},
});
```
useUploadFile
hook in a React component to upload files:// 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>
);
}
Files stored in R2 can be served to your users by generating a URL pointing to a given file.
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),
})),
);
},
});
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" />;
}
Files stored in R2 can be deleted from actions via the r2.delete
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 deleteByKey = mutation({
args: {
key: v.string(),
},
handler: async (ctx, args) => {
return await r2.deleteByKey(args.key);
},
});
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 uploadContentLength
: the size of the file in bytesLastModified
: the last modified date of the fileMetadata can be listed or paginated from actions via r2.listMetadata
and r2.pageMetadata
.
```ts // 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); }, });