### Install Dependencies and Run Locally Source: https://github.com/get-convex/r2/blob/main/CONTRIBUTING.md Install project dependencies and start the development server. ```sh npm i npm run dev ``` -------------------------------- ### Build One-Off Package Source: https://github.com/get-convex/r2/blob/main/CONTRIBUTING.md Clean the project, install exact dependencies, and create a package for distribution. ```sh npm run clean npm ci npm pack ``` -------------------------------- ### Install Convex R2 Component Source: https://github.com/get-convex/r2/blob/main/README.md Command to install the Convex R2 component package using npm. This is the first step in integrating R2 storage into your Convex project. ```bash npm install @convex-dev/r2 ``` -------------------------------- ### Configure Convex R2 Component in convex.config.ts Source: https://github.com/get-convex/r2/blob/main/README.md Example configuration for `convex/convex.config.ts` to integrate the R2 component. This involves importing `r2` and using `app.use(r2)`. ```typescript // 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; ``` -------------------------------- ### Configure Multiple R2 Buckets Source: https://github.com/get-convex/r2/blob/main/README.md Example demonstrating how to configure and use multiple R2 buckets within a Convex application. It shows creating separate `R2` instances with explicit configuration for an archive bucket, overriding environment variables. ```typescript 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!, }); ``` -------------------------------- ### React to R2 Metadata Syncs with Callbacks Source: https://context7.com/get-convex/r2/llms.txt Export `onSyncMetadata` from `r2.clientApi()` to hook into every metadata upsert. This example flags oversized uploads for review. ```typescript // convex/example.ts import { R2, type R2Callbacks } from "@convex-dev/r2"; import { components, internal } from "./_generated/api"; import type { DataModel } from "./_generated/dataModel"; const r2 = new R2(components.r2); const callbacks: R2Callbacks = internal.example; export const { generateUploadUrl, syncMetadata, onSyncMetadata } = r2.clientApi({ callbacks, onSyncMetadata: async (ctx, { bucket, key, isNew }) => { // isNew = true when this key is seen for the first time const meta = await r2.getMetadata(ctx, key); if (isNew && meta && meta.size && meta.size > 10 * 1024 * 1024) { // Flag oversized uploads for review await ctx.db.insert("oversizedUploads", { key, bucket, size: meta.size }); } }, }); ``` -------------------------------- ### r2.getUrl(key, options?) Source: https://context7.com/get-convex/r2/llms.txt Produces a time-limited pre-signed GET URL for serving a stored object. Default expiry is 900 seconds (15 minutes); maximum is 604 800 seconds (7 days). ```APIDOC ## `r2.getUrl(key, options?)` — Generate a Signed Download URL Produces a time-limited pre-signed GET URL for serving a stored object. Default expiry is 900 seconds (15 minutes); maximum is 604 800 seconds (7 days). ```ts // convex/messages.ts import { query } from "./_generated/server"; import { R2 } from "@convex-dev/r2"; import { components } from "./_generated/api"; const r2 = new R2(components.r2); export const listMessages = query({ args: {}, handler: async (ctx) => { const messages = await ctx.db.query("messages").collect(); return Promise.all( messages.map(async (msg) => ({ ...msg, // Short-lived URL for display in a React imageUrl: await r2.getUrl(msg.imageKey, { expiresIn: 3600 }), // → "https://.r2.cloudflarestorage.com/bucket/key?X-Amz-Expires=3600&..." })), ); }, }); ``` ``` -------------------------------- ### Configure Cloudflare R2 CORS Policy Source: https://github.com/get-convex/r2/blob/main/README.md Example JSON configuration for a Cloudflare R2 bucket's CORS policy. This policy allows GET and PUT requests from http://localhost:5173. Use '*' with caution for broader access. ```json [ { "AllowedOrigins": ["http://localhost:5173"], "AllowedMethods": ["GET", "PUT"], "AllowedHeaders": ["Content-Type"] } ] ``` -------------------------------- ### Low-Level XHR Upload with Progress and Retries Source: https://context7.com/get-convex/r2/llms.txt The underlying upload primitive used by `useUploadFile` hooks. This example shows how to implement custom retry logic with progress reporting. ```typescript // src/lib/upload.ts import { uploadWithProgress } from "@convex-dev/r2/react"; // same export from /svelte, /vue async function uploadWithRetry(url: string, file: File): Promise { const MAX_RETRIES = 3; for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { await uploadWithProgress(url, file, ({ loaded, total }) => { console.log(`Attempt ${attempt}: ${Math.round((loaded / total) * 100)}%`); }); return; // success } catch (err) { if (attempt === MAX_RETRIES) throw err; await new Promise((r) => setTimeout(r, 500 * attempt)); // back-off } } } ``` -------------------------------- ### React: Upload File with Progress Source: https://context7.com/get-convex/r2/llms.txt Use the `useUploadFile` hook in React to get an async function for uploading files. It supports progress callbacks and returns the R2 object key. Ensure the `api.example` route is correctly configured. ```tsx import { useUploadFile } from "@convex-dev/r2/react"; import { api } from "../convex/_generated/api"; import { useState } from "react"; export default function App() { const uploadFile = useUploadFile(api.example); const [progress, setProgress] = useState(null); async function handleChange(e: React.ChangeEvent) { const file = e.target.files![0]; setProgress(0); const key = await uploadFile(file, { onProgress: ({ loaded, total }) => setProgress(Math.round((loaded / total) * 100)), }); setProgress(null); console.log("Uploaded, key:", key); // e.g. "a3f2c1d0-..." } return ( <> {progress !== null &&

Uploading: {progress}%

} ); } ``` -------------------------------- ### Get Metadata Source: https://github.com/get-convex/r2/blob/main/README.md File metadata of an R2 file can be accessed from actions via `r2.getMetadata`. ```APIDOC ## getMetadata ### Description Retrieves metadata for a specific file in R2 storage. ### Method query ### Parameters #### Path Parameters - **key** (string) - Required - The object key of the file. ### Request Example ```json { "key": "path/to/your/file.jpg" } ``` ### Response #### Success Response (200) - **ContentType** (string) - The ContentType of the file if it was provided on upload. - **ContentLength** (number) - The size of the file in bytes. - **LastModified** (string) - The last modified date of the file. ### Response Example ```json { "ContentType": "image/jpeg", "ContentLength": 125338, "LastModified": "2024-03-20T12:34:56Z" } ``` ``` -------------------------------- ### Generate Signed Download URL with R2 Source: https://context7.com/get-convex/r2/llms.txt Produces a time-limited pre-signed GET URL for serving a stored object. The default expiry is 900 seconds, with a maximum of 604,800 seconds (7 days). ```typescript // convex/messages.ts import { query } from "./_generated/server"; import { R2 } from "@convex-dev/r2"; import { components } from "./_generated/api"; const r2 = new R2(components.r2); export const listMessages = query({ args: {}, handler: async (ctx) => { const messages = await ctx.db.query("messages").collect(); return Promise.all( messages.map(async (msg) => ({ ...msg, // Short-lived URL for display in a React imageUrl: await r2.getUrl(msg.imageKey, { expiresIn: 3600 }), // → "https://.r2.cloudflarestorage.com/bucket/key?X-Amz-Expires=3600&..." })), ); }, }); ``` -------------------------------- ### Get R2 File Metadata Source: https://github.com/get-convex/r2/blob/main/README.md Retrieves metadata for a specific file stored in an R2 bucket using its object key. This is useful for inspecting file properties like content type and size. ```typescript // 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); }, }); ``` -------------------------------- ### Deploy New Version Source: https://github.com/get-convex/r2/blob/main/CONTRIBUTING.md Release a new version of the project using the standard release script. ```sh npm run release ``` -------------------------------- ### R2 Constructor Source: https://context7.com/get-convex/r2/llms.txt Instantiates the server-side R2 client. Credentials can be read from environment variables or passed directly in the options. ```APIDOC ## new R2(component, options?) ### Description Instantiates the server-side R2 client. Credentials are read from environment variables by default; pass `options` to override individual fields or to configure a second bucket. The `client` getter lazily initialises the underlying AWS S3Client. ### Parameters - **component**: The Convex component instance. - **options?**: Optional configuration object for bucket settings. - **bucket**: (string) The R2 bucket name. - **endpoint**: (string) The R2 endpoint URL. - **accessKeyId**: (string) The R2 access key ID. - **secretAccessKey**: (string) The R2 secret access key. ### Example ```ts // convex/files.ts import { R2 } from "@convex-dev/r2"; import { components } from "./_generated/api"; // Primary bucket — uses R2_* environment variables export const r2 = new R2(components.r2); // Archive bucket — explicit credentials export 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!, }); // Access the raw S3Client if needed const s3 = r2.client; // lazily created, cached thereafter ``` ``` -------------------------------- ### Upload and Access Files with React and Convex R2 Source: https://github.com/get-convex/r2/blob/main/README.md Demonstrates how to upload files from a React frontend using `useUploadFile` and access them on the server via an R2 URL. Ensure the `api.example` is correctly configured. ```typescript // 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); ``` -------------------------------- ### Run Project Tests Source: https://github.com/get-convex/r2/blob/main/CONTRIBUTING.md Execute various testing and quality assurance commands for the project. ```sh npm run clean npm run build npm run typecheck npm run lint npm run test ``` -------------------------------- ### r2.clientApi(opts?) Source: https://context7.com/get-convex/r2/llms.txt Generates a set of Convex mutations and queries for client-side interaction, managing the full signed-URL upload flow. ```APIDOC ## r2.clientApi(opts?) ### Description Generates a set of Convex mutations and queries (`generateUploadUrl`, `syncMetadata`, `onSyncMetadata`, `getMetadata`, `listMetadata`, `deleteObject`) that should be exported from a file in `convex/`. Pass the `DataModel` generic for fully-typed `ctx` in every callback. Export the result and pass it to `useUploadFile` on the client side. ### Parameters - **DataModel**: A generic type representing the application's data model. - **opts**: Configuration object for callbacks. - **checkUpload**: (function) Called before issuing the upload URL. Throw to reject. - **checkReadKey**: (function) Authorization check for reading keys. - **checkReadBucket**: (function) Authorization check for reading buckets. - **checkDelete**: (function) Authorization check for deleting objects. - **onUpload**: (function) Called inside `syncMetadata` after a successful upload. - **onSyncMetadata**: (function) Called inside `syncMetadata` after R2 metadata is written to Convex. - **onDelete**: (function) Called inside `deleteObject` after the object is deleted from R2. - **callbacks**: An object containing internal Convex functions for callbacks. ### Example ```ts // convex/example.ts import { R2, R2Callbacks } from "@convex-dev/r2"; import { components, internal } from "./_generated/api"; import type { DataModel } from "./_generated/dataModel"; const r2 = new R2(components.r2); const callbacks: R2Callbacks = internal.example; // wire onSyncMetadata back export const { generateUploadUrl, syncMetadata, onSyncMetadata, getMetadata, listMetadata, deleteObject, } = r2.clientApi({ // Called before issuing the upload URL (throw to reject) checkUpload: async (ctx, bucket) => { // const user = await userFromAuth(ctx); // if (!user) throw new Error("Unauthorized"); }, checkReadKey: async (ctx, bucket, key) => { /* auth check */ }, checkReadBucket: async (ctx, bucket) => { /* auth check */ }, checkDelete: async (ctx, bucket, key) => { /* auth check */ }, // Called inside syncMetadata after a successful upload onUpload: async (ctx, bucket, key) => { await ctx.db.insert("images", { bucket, key }); }, // Called inside syncMetadata after R2 metadata is written to Convex onSyncMetadata: async (ctx, { bucket, key, isNew }) => { if (isNew) console.log("New file uploaded:", key); }, onDelete: async (ctx, bucket, key) => { const img = await ctx.db .query("images") .withIndex("bucket_key", q => q.eq("bucket", bucket).eq("key", key)) .unique(); if (img) await ctx.db.delete("images", img._id); }, callbacks, // enables onSyncMetadata to run as an internal mutation }); ``` ``` -------------------------------- ### Instantiating the Server-Side R2 Client Source: https://context7.com/get-convex/r2/llms.txt Instantiates the server-side R2 client. Credentials are read from environment variables by default; pass `options` to override individual fields or to configure a second bucket. The `client` getter lazily initialises the underlying AWS S3Client. ```typescript // convex/files.ts import { R2 } from "@convex-dev/r2"; import { components } from "./_generated/api"; // Primary bucket — uses R2_* environment variables export const r2 = new R2(components.r2); // Archive bucket — explicit credentials export 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!, }); // Access the raw S3Client if needed const s3 = r2.client; // lazily created, cached thereafter ``` -------------------------------- ### Svelte: Upload File with Progress Source: https://context7.com/get-convex/r2/llms.txt The Svelte equivalent of `useUploadFile` hook, internally using `useConvexClient`. It allows file uploads with progress indication. The `api.example` route must be set up. ```svelte { selectedFile = e.currentTarget.files?.[0] ?? null; }} /> {#if progress !== null}

Uploading: {progress}%

{/if} ``` -------------------------------- ### Vue: Upload File with Progress Source: https://context7.com/get-convex/r2/llms.txt A Vue composition API hook for file uploads, leveraging `convex-vue`'s `useConvexClient`. It provides an upload function with progress tracking. Ensure `api.example` is correctly defined. ```vue ``` -------------------------------- ### Deploy Alpha Release Source: https://github.com/get-convex/r2/blob/main/CONTRIBUTING.md Release an alpha version of the project using the alpha release script. ```sh npm run alpha ``` -------------------------------- ### List All Metadata with R2 Source: https://context7.com/get-convex/r2/llms.txt Retrieves a paginated list of metadata documents for all objects in the configured bucket, ordered by insertion time. Supports pagination with `limit` and `cursor` arguments. ```typescript // convex/admin.ts import { query } from "./_generated/server"; import { v } from "convex/values"; import { R2 } from "@convex-dev/r2"; import { components } from "./_generated/api"; const r2 = new R2(components.r2); export const listAllFiles = query({ args: { limit: v.optional(v.number()), cursor: v.optional(v.string()), }, handler: async (ctx, { limit, cursor }) => { const result = await r2.listMetadata(ctx, limit ?? 50, cursor); // result → { // page: [ { key, contentType, size, lastModified, url, bucketLink, ... }, ... ], // isDone: false, // continueCursor: "eyJ...", // } return result; }, }); ``` -------------------------------- ### r2.listMetadata(ctx, limit?, cursor?) Source: https://context7.com/get-convex/r2/llms.txt Retrieves a paginated list of metadata documents for every object in the configured bucket, ordered by insertion time. ```APIDOC ## `r2.listMetadata(ctx, limit?, cursor?)` — List All Metadata Retrieves a paginated list of metadata documents for every object in the configured bucket, ordered by insertion time. ```ts // convex/admin.ts import { query } from "./_generated/server"; import { v } from "convex/values"; import { R2 } from "@convex-dev/r2"; import { components } from "./_generated/api"; const r2 = new R2(components.r2); export const listAllFiles = query({ args: { limit: v.optional(v.number()), cursor: v.optional(v.string()), }, handler: async (ctx, { limit, cursor }) => { const result = await r2.listMetadata(ctx, limit ?? 50, cursor); // result → { // page: [ { key, contentType, size, lastModified, url, bucketLink, ... }, ... ], // isDone: false, // continueCursor: "eyJ...", // } return result; }, }); ``` ``` -------------------------------- ### Exposing R2 Mutations and Queries to the Client Source: https://context7.com/get-convex/r2/llms.txt Generates a set of Convex mutations and queries that should be exported from a file in `convex/`. Pass the `DataModel` generic for fully-typed `ctx` in every callback. Export the result and pass it to `useUploadFile` on the client side. ```typescript // convex/example.ts import { R2, R2Callbacks } from "@convex-dev/r2"; import { components, internal } from "./_generated/api"; import type { DataModel } from "./_generated/dataModel"; const r2 = new R2(components.r2); const callbacks: R2Callbacks = internal.example; // wire onSyncMetadata back export const { generateUploadUrl, syncMetadata, onSyncMetadata, getMetadata, listMetadata, deleteObject, } = r2.clientApi({ // Called before issuing the upload URL (throw to reject) checkUpload: async (ctx, bucket) => { // const user = await userFromAuth(ctx); // if (!user) throw new Error("Unauthorized"); }, checkReadKey: async (ctx, bucket, key) => { /* auth check */ }, checkReadBucket: async (ctx, bucket) => { /* auth check */ }, checkDelete: async (ctx, bucket, key) => { /* auth check */ }, // Called inside syncMetadata after a successful upload onUpload: async (ctx, bucket, key) => { await ctx.db.insert("images", { bucket, key }); }, // Called inside syncMetadata after R2 metadata is written to Convex onSyncMetadata: async (ctx, { bucket, key, isNew }) => { if (isNew) console.log("New file uploaded:", key); }, onDelete: async (ctx, bucket, key) => { const img = await ctx.db .query("images") .withIndex("bucket_key", q => q.eq("bucket", bucket).eq("key", key)) .unique(); if (img) await ctx.db.delete("images", img._id); }, callbacks, // enables onSyncMetadata to run as an internal mutation }); ``` -------------------------------- ### Set R2 Environment Variables Source: https://github.com/get-convex/r2/blob/main/README.md Commands to set the necessary Cloudflare R2 API credentials and bucket name as environment variables for your Convex deployment. Replace 'xxxxx' with your actual credentials. ```bash 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 ``` -------------------------------- ### Build CDN URL from Object Key Source: https://github.com/get-convex/r2/blob/main/README.md Constructs a CDN-accessible URL for a file stored in R2 using its object key. Ensure you replace 'cdn.example.com' with your actual custom domain. This function encodes each segment of the key to ensure proper URL formation. ```typescript // 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), })); }, }); ``` -------------------------------- ### List Metadata Source: https://github.com/get-convex/r2/blob/main/README.md Metadata can be listed from actions via `r2.listMetadata`. ```APIDOC ## listMetadata ### Description Lists metadata for files in R2 storage, with an optional limit. ### Method query ### Parameters #### Query Parameters - **limit** (number) - Optional - The maximum number of metadata entries to return. ### Request Example ```json { "limit": 10 } ``` ### Response #### Success Response (200) - **metadataList** (array) - An array of metadata objects for the files. ### Response Example ```json [ { "ContentType": "image/jpeg", "ContentLength": 125338, "LastModified": "2024-03-20T12:34:56Z" } ] ``` ``` -------------------------------- ### CDN URL Helper Source: https://context7.com/get-convex/r2/llms.txt For public assets that benefit from Cloudflare's edge cache, build URLs directly from the object key against a custom domain connected to the R2 bucket instead of using `r2.getUrl()`. ```APIDOC ## CDN URL Helper — Permanent URLs via Custom Domain For public assets that benefit from Cloudflare's edge cache, build URLs directly from the object key against a custom domain connected to the R2 bucket instead of using `r2.getUrl()`. ```ts // convex/assets.ts import { query } from "./_generated/server"; import { R2 } from "@convex-dev/r2"; import { components } from "./_generated/api"; const r2 = new R2(components.r2); const CDN_BASE = "https://cdn.example.com"; function getCdnUrl(key: string): string { // URL-encode each path segment to handle keys with special characters return `${CDN_BASE}/${key.split("/").map(encodeURIComponent).join("/")}`; } export const listAvatars = query({ args: {}, handler: async (ctx) => { const users = await ctx.db.query("users").collect(); return users.map((user) => ({ ...user, avatarUrl: getCdnUrl(user.avatarKey), // → "https://cdn.example.com/avatars/user-123.jpg" (cached, permanent) })); }, }); ``` ``` -------------------------------- ### uploadWithProgress(url, file, onProgress?) Source: https://context7.com/get-convex/r2/llms.txt A low-level primitive for uploading files using XHR, used by `useUploadFile` hooks. It allows for full control over the XHR lifecycle, including cancel support and custom headers. ```APIDOC ## `uploadWithProgress(url, file, onProgress?)` — Low-Level XHR Upload The underlying upload primitive used by all `useUploadFile` hooks. Call it directly when you want full control over the XHR lifecycle (e.g., cancel support, custom headers via a fetch wrapper) without using the framework hooks. ### Parameters #### Path Parameters - **url** (string) - Required - The upload URL. - **file** (File) - Required - The file to upload. - **onProgress** (Function) - Optional - A callback function that receives progress information (`loaded`, `total`). ### Request Example ```ts // src/lib/upload.ts import { uploadWithProgress } from "@convex-dev/r2/react"; // same export from /svelte, /vue async function uploadWithRetry(url: string, file: File): Promise { const MAX_RETRIES = 3; for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { await uploadWithProgress(url, file, ({ loaded, total }) => { console.log(`Attempt ${attempt}: ${Math.round((loaded / total) * 100)}%`); }); return; // success } catch (err) { if (attempt === MAX_RETRIES) throw err; await new Promise((r) => setTimeout(r, 500 * attempt)); // back-off } } } ``` ``` -------------------------------- ### r2.syncMetadata(ctx, key) Source: https://context7.com/get-convex/r2/llms.txt Manually syncs metadata for a given key by fetching the HeadObject response from R2 and upserting its fields into the Convex metadata table. Useful for objects uploaded outside the normal flow. ```APIDOC ## `r2.syncMetadata(ctx, key)` — Manually Sync Metadata Fetches the `HeadObject` response from R2 for `key` and upserts its fields into the Convex metadata table. Useful when objects are uploaded outside the normal `useUploadFile` / `store` flow (e.g., a third-party tool placed a file in the bucket). ### Parameters #### Path Parameters - **ctx** (Object) - Required - The Convex context object. - **key** (string) - Required - The key of the object to sync metadata for. ### Request Example ```ts // convex/maintenance.ts import { internalAction } from "./_generated/server"; import { v } from "convex/values"; import { R2 } from "@convex-dev/r2"; import { components } from "./_generated/api"; const r2 = new R2(components.r2); export const backfillKey = internalAction({ args: { key: v.string() }, handler: async (ctx, { key }) => { // key was written directly to R2; pull its metadata into Convex await r2.syncMetadata(ctx, key); console.log("Metadata synced for", key); }, }); ``` ``` -------------------------------- ### r2.generateUploadUrl(customKey?) Source: https://context7.com/get-convex/r2/llms.txt Generates a signed PUT URL and key for direct client-side uploads when you need to control the object key. This method must be called from your own server-side mutation. ```APIDOC ## `r2.generateUploadUrl(customKey?)` — Custom-Key Upload URL Generates a signed PUT URL (and key) for direct client-side uploads when you need to control the object key. Unlike the `clientApi` version, this method must be called from your own server-side mutation. ```ts // convex/uploads.ts import { mutation } from "./_generated/server"; import { R2 } from "@convex-dev/r2"; import { components } from "./_generated/api"; const r2 = new R2(components.r2); export const getUploadUrlForUser = mutation({ args: {}, handler: async (ctx) => { const user = await ctx.auth.getUserIdentity(); if (!user) throw new Error("Unauthenticated"); // Build a deterministic-ish key scoped to the user const key = `users/${user.subject}/${crypto.randomUUID()}`; const { url, key: assignedKey } = await r2.generateUploadUrl(key); // { url: "https:///bucket/users/abc/uuid?X-Amz-Signature=...", key: "users/abc/uuid" } return { url, key: assignedKey }; }, }); ``` ``` -------------------------------- ### onSyncMetadata Callback Source: https://github.com/get-convex/r2/blob/main/README.md 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. ```APIDOC ## onSyncMetadata ### Description Callback function executed after R2 metadata is synced. It receives arguments including the bucket, key, and whether the object is new, and can be used to access and process the metadata of the synced object. ### Parameters #### Arguments - **ctx** (object) - The Convex context. - **args** (object) - Arguments object. - **bucket** (string) - The name of the R2 bucket. - **key** (string) - The object key of the synced file. - **isNew** (boolean) - True if the key did not previously exist in your Convex R2 metadata table. ### Example Usage ```typescript // 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( { 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); }, }, ); ``` ``` -------------------------------- ### r2.store for Server-Side Uploads Source: https://context7.com/get-convex/r2/llms.txt Uploads a Blob, Buffer, or Uint8Array to R2 directly from a Convex action. This method syncs metadata and returns the object key, suitable for server-generated or server-downloaded files. ```APIDOC ## `r2.store(ctx, file, opts?)` — Server-Side Upload from Actions Uploads a `Blob`, `Buffer`, or `Uint8Array` to R2 directly from a Convex action, syncs metadata, and returns the object key. Use this for server-generated or server-downloaded files. ```ts // convex/jobs.ts import { internalAction } from "./_generated/server"; import { internal } from "./_generated/api"; import { R2 } from "@convex-dev/r2"; import { components } from "./_generated/api"; const r2 = new R2(components.r2); export const processAndStore = internalAction({ args: {}, handler: async (ctx) => { // Download an image from an external source const response = await fetch("https://picsum.photos/800/600"); const blob = await response.blob(); // Upload to R2 with optional metadata const key = await r2.store(ctx, blob, { key: "reports/daily-thumbnail.jpg", // custom key (must be unique) type: "image/jpeg", // override MIME type disposition: "inline; filename=thumbnail.jpg", cacheControl: "public, max-age=86400", }); // key → "reports/daily-thumbnail.jpg" await ctx.runMutation(internal.jobs.recordReport, { key }); }, }); ``` ``` -------------------------------- ### r2.getMetadata(ctx, key) Source: https://context7.com/get-convex/r2/llms.txt Returns object metadata stored in Convex (content type, size, SHA-256, last modified, Cloudflare dashboard link, and a signed URL). Returns `null` if the key is not in the metadata table. ```APIDOC ## `r2.getMetadata(ctx, key)` — Read Cached Object Metadata Returns object metadata stored in Convex (content type, size, SHA-256, last modified, Cloudflare dashboard link, and a signed URL). Returns `null` if the key is not in the metadata table. ```ts // convex/files.ts import { v } from "convex/values"; import { query } from "./_generated/server"; import { R2 } from "@convex-dev/r2"; import { components } from "./_generated/api"; const r2 = new R2(components.r2); export const getFileInfo = query({ args: { key: v.string() }, handler: async (ctx, { key }) => { const meta = await r2.getMetadata(ctx, key); if (!meta) return null; // meta → { // key: "reports/daily-thumbnail.jpg", // contentType: "image/jpeg", // size: 125338, // sha256: "abc123...", // lastModified: "2024-03-20T12:34:56.000Z", // bucket: "my-bucket", // link: "https://dash.cloudflare.com/...", // url: "https://...", // signed GET URL // bucketLink: "https://dash.cloudflare.com/.../r2/default/buckets/my-bucket", // } return meta; }, }); ``` ``` -------------------------------- ### CDN URL Helper for Permanent Asset URLs Source: https://context7.com/get-convex/r2/llms.txt Builds permanent URLs for public assets using a custom domain connected to the R2 bucket, leveraging Cloudflare's edge cache. This is an alternative to using `r2.getUrl()` for publicly accessible assets. ```typescript // convex/assets.ts import { query } from "./_generated/server"; import { R2 } from "@convex-dev/r2"; import { components } from "./_generated/api"; const r2 = new R2(components.r2); const CDN_BASE = "https://cdn.example.com"; function getCdnUrl(key: string): string { // URL-encode each path segment to handle keys with special characters return `${CDN_BASE}/${key.split("/").map(encodeURIComponent).join("/")}`; } export const listAvatars = query({ args: {}, handler: async (ctx) => { const users = await ctx.db.query("users").collect(); return users.map((user) => ({ ...user, avatarUrl: getCdnUrl(user.avatarKey), // → "https://cdn.example.com/avatars/user-123.jpg" (cached, permanent) })); }, }); ``` -------------------------------- ### List and Paginate R2 Metadata Source: https://github.com/get-convex/r2/blob/main/README.md Provides functions to list and paginate through metadata of files stored in an R2 bucket. Use `listMetadata` for a simple list up to a limit, and `pageMetadata` for paginated results. ```typescript // 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); }, }); ``` -------------------------------- ### Page Metadata Source: https://github.com/get-convex/r2/blob/main/README.md Metadata can be paginated from actions via `r2.pageMetadata`. ```APIDOC ## pageMetadata ### Description Pages through metadata for files in R2 storage. ### Method query ### Parameters #### Request Body - **paginationOpts** (object) - Required - Options for pagination. - **cursor** (string) - Optional - The cursor for the next page. - **limit** (number) - Optional - The maximum number of entries per page. ### Request Example ```json { "paginationOpts": { "cursor": "someCursor", "limit": 10 } } ``` ### Response #### Success Response (200) - **paginatedMetadata** (object) - An object containing paginated metadata. - **values** (array) - An array of metadata objects. - **nextPageCursor** (string) - The cursor for the next page of results. ### Response Example ```json { "values": [ { "ContentType": "image/jpeg", "ContentLength": 125338, "LastModified": "2024-03-20T12:34:56Z" } ], "nextPageCursor": "anotherCursor" } ``` ``` -------------------------------- ### Instantiate R2 Component Client Source: https://github.com/get-convex/r2/blob/main/README.md Instantiate the R2 component client in your Convex project. Pass your DataModel type as a generic parameter for proper TypeScript typing in callbacks. The `checkUpload` and `onUpload` callbacks allow for custom validation and post-upload logic. ```typescript // 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({ 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. }, }); ``` -------------------------------- ### Registering the R2 Component in Convex Source: https://context7.com/get-convex/r2/llms.txt Register the R2 component in your `convex/convex.config.ts` file and set the required environment variables before using any other API. ```typescript // 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; ``` ```sh npm install @convex-dev/r2 npx convex env set R2_TOKEN npx convex env set R2_ACCESS_KEY_ID npx convex env set R2_SECRET_ACCESS_KEY npx convex env set R2_ENDPOINT npx convex env set R2_BUCKET ``` -------------------------------- ### Generate File URL in Query Source: https://github.com/get-convex/r2/blob/main/README.md Generate a file URL from an object key within a Convex query using `r2.getUrl`. This is useful for serving files directly to your application. The `expiresIn` option can customize the URL's validity period. ```typescript // 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 }, ), })), ); }, }); ``` -------------------------------- ### Store File from Action Source: https://github.com/get-convex/r2/blob/main/README.md Store files directly from server-side actions using `r2.store`. This method uploads a blob, syncs metadata, and returns the object key. Optionally provide a custom key, MIME type, Content-Disposition, and Cache-Control headers. ```typescript // 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 }); }, }); ``` -------------------------------- ### Render Image from URL Source: https://github.com/get-convex/r2/blob/main/README.md A React component to display an image using a provided URL. This is a common pattern for rendering files served from R2. ```tsx // src/App.tsx function Image({ message }: { message: { url: string } }) { return ; } ```