Summary
Uploaded profile pictures disappear after approximately 1 hour due to R2 presigned URL expiration. The presigned URL is stored directly in the database instead of generating it on-demand.
Root Cause
In src/utils/storage/r2.ts (lines 101, 113), presigned URLs are configured to expire in 1 hour:
expiresIn: 3600 // 1 hour
The upload flow in src/fn/profiles.ts (lines 73-99) returns both:
imageKey - the permanent R2 path (profiles/{userId}/{timestamp}-{fileName})
imageUrl - the presigned URL (expires in 1 hour)
In src/routes/profile/edit.tsx (lines 234-239), both are saved to the database:
updateProfileMutation({ imageId: imageKey, image: imageUrl })
The image field (presigned URL) is then used for display, which breaks after 1 hour.
Steps to Reproduce
- Go to profile edit page (
/profile/edit)
- Upload a profile picture
- Confirm the picture displays correctly
- Wait ~1 hour
- Refresh the page - image is broken
Current Behavior
Profile picture displays initially but returns 403/broken after presigned URL expires (~1 hour).
Expected Behavior
Profile picture should persist indefinitely after upload.
Proposed Solution
The imageId (R2 key) is already stored correctly. The fix should:
- Generate presigned URLs on-demand when rendering profile images
- Or use public bucket URLs if images don't require access control
- Update all places that read
profile.image to generate fresh URLs from profile.imageId
Affected Areas
Acceptance Criteria
Summary
Uploaded profile pictures disappear after approximately 1 hour due to R2 presigned URL expiration. The presigned URL is stored directly in the database instead of generating it on-demand.
Root Cause
In
src/utils/storage/r2.ts(lines 101, 113), presigned URLs are configured to expire in 1 hour:The upload flow in
src/fn/profiles.ts(lines 73-99) returns both:imageKey- the permanent R2 path (profiles/{userId}/{timestamp}-{fileName})imageUrl- the presigned URL (expires in 1 hour)In
src/routes/profile/edit.tsx(lines 234-239), both are saved to the database:The
imagefield (presigned URL) is then used for display, which breaks after 1 hour.Steps to Reproduce
/profile/edit)Current Behavior
Profile picture displays initially but returns 403/broken after presigned URL expires (~1 hour).
Expected Behavior
Profile picture should persist indefinitely after upload.
Proposed Solution
The
imageId(R2 key) is already stored correctly. The fix should:profile.imageto generate fresh URLs fromprofile.imageIdAffected Areas
/src/routes/profile/edit.tsx(upload handling)/src/routes/profile/$userId.tsx(profile display)/src/routes/members.tsx(user list avatars)/src/fn/profiles.ts(presigned URL generation)/src/utils/storage/r2.ts(1-hour expiry config)/src/db/schema.ts-profilestable stores bothimageandimageIdAcceptance Criteria
imageIdto generate fresh presigned URLs on-demand (or switch to public URLs)