ulysses
ulysses3mo ago

oh i see. do you have an example of this

oh i see. do you have an example of this? are the env vars the same for multiple installations?
3 Replies
erquhart
erquhart3mo ago
I don't have an example to point to, but the use method takes an optional object with a name for this purpose:
// convex/convex.config.ts
import { defineApp } from "convex/server";
import r2 from "@convex-dev/r2/convex.config";

const app = defineApp();
app.use(r2, { name: "bucket1" });
app.use(r2, { name: "bucket2" });

export default app;
// convex/convex.config.ts
import { defineApp } from "convex/server";
import r2 from "@convex-dev/r2/convex.config";

const app = defineApp();
app.use(r2, { name: "bucket1" });
app.use(r2, { name: "bucket2" });

export default app;
And then you would do the whole component initialization twice, probably in two different files just to keep it straight, but you could it one file also. When you call new R2() both instances will default to the same env vars, so you probably want to define all of the env vars inline (you can still use env vars, just specify inline):
const r2bucket1 = new R2(components.bucket1, {
R2_BUCKET: process.env.R2_BUCKET_1,
R2_ENDPOINT: process.env.R2_ENDPOINT_1,
R2_ACCESS_KEY_ID: process.env.R2_ACCESS_KEY_ID_1,
R2_SECRET_ACCESS_KEY: process.env.R2_SECRET_ACCESS_KEY_1,
});
const r2bucket2 = new R2(components.bucket2, {
R2_BUCKET: process.env.R2_BUCKET_2,
R2_ENDPOINT: process.env.R2_ENDPOINT_2,
R2_ACCESS_KEY_ID: process.env.R2_ACCESS_KEY_ID_2,
R2_SECRET_ACCESS_KEY: process.env.R2_SECRET_ACCESS_KEY_2,
});
const r2bucket1 = new R2(components.bucket1, {
R2_BUCKET: process.env.R2_BUCKET_1,
R2_ENDPOINT: process.env.R2_ENDPOINT_1,
R2_ACCESS_KEY_ID: process.env.R2_ACCESS_KEY_ID_1,
R2_SECRET_ACCESS_KEY: process.env.R2_SECRET_ACCESS_KEY_1,
});
const r2bucket2 = new R2(components.bucket2, {
R2_BUCKET: process.env.R2_BUCKET_2,
R2_ENDPOINT: process.env.R2_ENDPOINT_2,
R2_ACCESS_KEY_ID: process.env.R2_ACCESS_KEY_ID_2,
R2_SECRET_ACCESS_KEY: process.env.R2_SECRET_ACCESS_KEY_2,
});
ulysses
ulyssesOP2mo ago
convex/r2/profilePictures.ts

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

// Dedicated R2 instance for profile pictures
export const profilePicturesR2 = new R2(components.profilePictures, {
R2_BUCKET: process.env.R2_PFP_BUCKET,
R2_ENDPOINT: process.env.R2_ENDPOINT,
R2_ACCESS_KEY_ID: process.env.R2_ACCESS_KEY_ID,
R2_SECRET_ACCESS_KEY: process.env.R2_SECRET_ACCESS_KEY,
})

export const { generateUploadUrl, syncMetadata } = profilePicturesR2.clientApi({
checkUpload: async (ctx, bucket) => {
// For profile pictures, allow uploads during onboarding
// Just check that user is authenticated (but may not exist in DB yet)
const user = await ctx.auth.getUserIdentity()
if (!user) {
throw new Error('Authentication required for profile picture upload')
}

// Allow upload - don't require user to exist in database yet
// This enables onboarding uploads before user creation
},

onUpload: async (ctx, key) => {
console.log(`Profile picture uploaded with key: ${key}`)
}
})
convex/r2/profilePictures.ts

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

// Dedicated R2 instance for profile pictures
export const profilePicturesR2 = new R2(components.profilePictures, {
R2_BUCKET: process.env.R2_PFP_BUCKET,
R2_ENDPOINT: process.env.R2_ENDPOINT,
R2_ACCESS_KEY_ID: process.env.R2_ACCESS_KEY_ID,
R2_SECRET_ACCESS_KEY: process.env.R2_SECRET_ACCESS_KEY,
})

export const { generateUploadUrl, syncMetadata } = profilePicturesR2.clientApi({
checkUpload: async (ctx, bucket) => {
// For profile pictures, allow uploads during onboarding
// Just check that user is authenticated (but may not exist in DB yet)
const user = await ctx.auth.getUserIdentity()
if (!user) {
throw new Error('Authentication required for profile picture upload')
}

// Allow upload - don't require user to exist in database yet
// This enables onboarding uploads before user creation
},

onUpload: async (ctx, key) => {
console.log(`Profile picture uploaded with key: ${key}`)
}
})
convex/r2/media.ts

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

// R2 instance for general media uploads (messages, threads, etc.)
export const mediaR2 = new R2(components.media, {
R2_BUCKET: process.env.R2_MEDIA_BUCKET,
R2_ENDPOINT: process.env.R2_ENDPOINT,
R2_ACCESS_KEY_ID: process.env.R2_ACCESS_KEY_ID,
R2_SECRET_ACCESS_KEY: process.env.R2_SECRET_ACCESS_KEY
})

export const { generateUploadUrl, syncMetadata } = mediaR2.clientApi({
checkUpload: async (ctx, bucket) => {
// Get current user from auth
const user = await ctx.auth.getUserIdentity()
if (!user) {
throw new Error('Authentication required for file upload')
}

// Validate user exists in database
const userRecord = await ctx.db
.query('users')
.withIndex('by_privy_id', (q) => q.eq('privy_id', user.subject))
.first()

if (!userRecord) {
throw new Error('User not found in database')
}
},

onUpload: async (ctx, key) => {
console.log(`Media file uploaded with key: ${key}`)
}
})
convex/r2/media.ts

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

// R2 instance for general media uploads (messages, threads, etc.)
export const mediaR2 = new R2(components.media, {
R2_BUCKET: process.env.R2_MEDIA_BUCKET,
R2_ENDPOINT: process.env.R2_ENDPOINT,
R2_ACCESS_KEY_ID: process.env.R2_ACCESS_KEY_ID,
R2_SECRET_ACCESS_KEY: process.env.R2_SECRET_ACCESS_KEY
})

export const { generateUploadUrl, syncMetadata } = mediaR2.clientApi({
checkUpload: async (ctx, bucket) => {
// Get current user from auth
const user = await ctx.auth.getUserIdentity()
if (!user) {
throw new Error('Authentication required for file upload')
}

// Validate user exists in database
const userRecord = await ctx.db
.query('users')
.withIndex('by_privy_id', (q) => q.eq('privy_id', user.subject))
.first()

if (!userRecord) {
throw new Error('User not found in database')
}
},

onUpload: async (ctx, key) => {
console.log(`Media file uploaded with key: ${key}`)
}
})
import aggregate from '@convex-dev/aggregate/convex.config'
import migrations from '@convex-dev/migrations/convex.config'
import r2 from '@convex-dev/r2/convex.config'
import workpool from '@convex-dev/workpool/convex.config'
import { defineApp } from 'convex/server'

const app = defineApp()
app.use(aggregate, { name: 'fameAggregate' })
app.use(aggregate, { name: 'streakAggregate' })
app.use(workpool, { name: 'indexerWorkpool' })
app.use(r2, { name: 'media' }) // For authenticated uploads (messages, etc.)
app.use(r2, { name: 'profilePictures' }) // For profile picture uploads (including onboarding)
app.use(migrations)

export default app
import aggregate from '@convex-dev/aggregate/convex.config'
import migrations from '@convex-dev/migrations/convex.config'
import r2 from '@convex-dev/r2/convex.config'
import workpool from '@convex-dev/workpool/convex.config'
import { defineApp } from 'convex/server'

const app = defineApp()
app.use(aggregate, { name: 'fameAggregate' })
app.use(aggregate, { name: 'streakAggregate' })
app.use(workpool, { name: 'indexerWorkpool' })
app.use(r2, { name: 'media' }) // For authenticated uploads (messages, etc.)
app.use(r2, { name: 'profilePictures' }) // For profile picture uploads (including onboarding)
app.use(migrations)

export default app
hey @erquhart , I defined the r2 components as such, but they seem to both be defaulting to the same bucket (media) instead of different ones
erquhart
erquhart2mo ago
Can you try logging the variables where you instantiate the component class instances to confirm nothing unexpected is happening there Actually, you can log mediaR2.options and profilePicturesR2.options to get the exact values each ended up with.

Did you find this page helpful?