#Upload a file to R2 using vanilla js
17 messages · Page 1 of 1 (latest)
Hi @runic canopy. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave:
You can use the R2 component’s client API and a plain fetch flow. Define the R2 client API on your Convex backend to expose generateUploadUrl and syncMetadata, then from vanilla JS:
Backend (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) => {
// authorize uploads as needed
},
});
(Using r2.clientApi is required for the signed URL + metadata sync flow) (Uploading files).
Frontend (vanilla JS)
<!doctype html>
<input id="file" type="file" />
<button id="upload">Upload</button>
<script src="<https://unpkg.com/[email protected]/dist/browser.bundle.js"></script>>
<script>
const CONVEX_URL = "YOUR_CONVEX_URL";
const client = new convex.ConvexClient(CONVEX_URL);
const api = convex.anyApi; // provides access to your generated API routes
document.getElementById("upload").addEventListener("click", async () => {
const file = document.getElementById("file").files[0];
if (!file) return;
// 1) Ask Convex for a signed upload URL
const { url, key } = await client.mutation(api.example.generateUploadUrl, {});
// 2) Upload the file directly to R2
const put = await fetch(url, {
method: "PUT",
headers: { "Content-Type": file.type },
body: file,
});
if (!put.ok) {
console.error("Upload failed");
return;
}
// 3) Tell Convex to sync the object’s metadata into your DB
await client.mutation(api.example.syncMetadata, { key });
// key is your R2 object key
console.log("Uploaded to R2 with key:", key);
});
</script>
Notes:
- The vanilla flow mirrors what the React/Svelte hooks do: generate a signed URL, PUT the file to R2, then sync metadata in Convex (Uploading files).
- If you prefer server-side uploads (no signed URL), you can upload in an action with r2.store, but that’s server code, not vanilla JS in the browser (Storing files from actions).
- The Convex browser bundle + ConvexClient pattern for script tags is shown here; you can call your generated mutations the same way from plain JS (Script Tag).
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@runic canopy Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@glacial snow If that signed url expires how are you going to create a new one?
how do you upload with a key that you defined?
Let me check the knowledge base for you again. One moment! :books:
- Refreshing an expired signed URL:
- Generate a new one the same way you did originally: call your Convex mutation that wraps r2.clientApi’s generateUploadUrl, then retry the PUT and syncMetadata steps. Signed URLs are short‑lived by design; fetch them shortly before uploading and regenerate on failure/expiry (Uploading files).
- If you control the server flow, you can also upload from an action with r2.store (no client URL needed) (Storing from actions).
- Uploading with a custom key you define:
- From server-side code:
- r2.store accepts an options object with key and type, so you can set your own key when uploading from an action:
(Storing from actions).const key = await r2.store(ctx, blobOrBuffer, { key: "my/custom/key", type: "image/jpeg" });
- r2.store accepts an options object with key and type, so you can set your own key when uploading from an action:
- From client-side (browser) with signed URLs:
- The client-side generateUploadUrl exposed via r2.clientApi does not accept a custom key to avoid letting the client define keys. To use a custom key from the browser, create your own Convex mutation that calls r2.generateUploadUrl(customKey) on the server, then use that URL for the PUT and call syncMetadata with the same key:
Then PUT the file to the returned URL and call syncMetadata(key) after upload (Using a custom object key, Uploading files).// convex/example.ts export const generateUploadUrlWithCustomKey = mutation({ args: {}, handler: async (ctx) => { const key = `userScoped/${crypto.randomUUID()}`; return r2.generateUploadUrl(key); }, });
- The client-side generateUploadUrl exposed via r2.clientApi does not accept a custom key to avoid letting the client define keys. To use a custom key from the browser, create your own Convex mutation that calls r2.generateUploadUrl(customKey) on the server, then use that URL for the PUT and call syncMetadata with the same key:
- From server-side code:
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@runic canopy Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@glacial snow Does this work below?
// convex/example.ts
import { R2 } from "@convex-dev/r2";
import { mutation } from "@/convex/_generated/server";
import { components } from "@/convex/_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 ctx.auth.getUserIdentity();
if (!currentUser) {
throw new Error("User not found");
}
const userObjectId = await ctx.db.insert("userObjects", {
userId: currentUser.subject,
objectType: "IMAGE",
objectSource: "USER_CREATED",
isAiCreated: false,
isObjectUserConfirmed: true,
});
const key = `${currentUser.subject}.${userObjectId}`;
return r2.generateUploadUrl(key);
},
});
Let me check the knowledge base for you again. One moment! :books:
Mostly yes, with two notes:
-
Using a custom key from a server mutation is the recommended pattern. The R2 instance supports r2.generateUploadUrl(key) where you supply the key, and the example in the docs mirrors your approach (server decides the key). If the key already exists, generation will fail as noted (Using a custom object key) (Using a custom object key).
-
After the browser uploads to the returned URL, you still need to sync metadata with your Convex DB by calling the r2 client API’s syncMetadata with the same key (this is what the React/Svelte hooks automate). If you’re rolling your own flow, make sure to invoke syncMetadata after the PUT to R2 (Uploading files) (Uploading files).
Your mutation’s auth check via ctx.auth.getUserIdentity and key format ${currentUser.subject}.${userObjectId} are fine. Just ensure:
- userObjects is a valid table and the insert succeeds before composing the key.
- You handle the case where the key already exists (the mutation will fail) (Using a custom object key) (Using a custom object key).
Note: you can ask me a follow up question by @ mentioning me again :speech_balloon:
@runic canopy Kapa.ai is still learning and improving, please let me know how I did by reacting below.
@glacial snow How do you sync the metadata?
// 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, 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.
},
});
Let me check the knowledge base for you again. One moment! :books:
Call the syncMetadata mutation from your client after the upload finishes (the same key returned/generated for the upload). That’s exactly what the React/Svelte hooks automate; in vanilla flows you do it explicitly:
-
Server: expose syncMetadata via r2.clientApi as you’ve done (generateUploadUrl, syncMetadata) (Uploading files).
-
Client flow:
- get {url, key} from your mutation that calls r2.generateUploadUrl(...)
- PUT/POST the file to url
- call syncMetadata with { key } to persist the object’s metadata in Convex