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.
- Create a Cloudflare account
- Create an R2 bucket
- Set the bucket name as an environment variable
R2_BUCKET
in your Convex deployment - 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
- Token Value:
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:
- generates the signed url
- uploads the file to R2
- stores the file's metadata in your Convex database
- 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";
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.
},
});
- Use the
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 file
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);
},
});
<!-- END: Include on https://convex.dev/components -->