feat(upload): add pushduck + Cloudflare R2 avatar upload#923
Open
abhay-ramesh wants to merge 1 commit intoslopus:mainfrom
Open
feat(upload): add pushduck + Cloudflare R2 avatar upload#923abhay-ramesh wants to merge 1 commit intoslopus:mainfrom
abhay-ramesh wants to merge 1 commit intoslopus:mainfrom
Conversation
Replaces the planned server-proxied upload flow with a direct
client-to-R2 presigned URL flow, so image bytes never transit
the server.
Server (packages/happy-server):
- Add uploadRoutes.ts: pushduck router with `avatarUpload` route
- cloudflareR2 provider (CLOUDFLARE_R2_* env vars)
- Bearer-token auth inside pushduck middleware
- Key pattern: public/users/{userId}/avatars/{random12}.{ext}
- onUploadComplete: fetch image from R2 → Sharp (thumbhash +
dimensions) → DB transaction (delete old, insert new
UploadedFile, update Account.avatar) → real-time socket update
- Graceful 503 when R2 env vars are not set
- Register uploadRoutes in api.ts (GET + POST /v1/upload)
- Document R2 env vars in .env.dev
App (packages/happy-app):
- Add apiUpload.ts: plain async uploadAvatar() using the
presign → PUT → complete pattern (React Native compatible)
- account.tsx: avatar item always visible in Profile section;
image picker with square crop → uploadAvatar → refreshProfile
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #417
Problem
Avatar uploads had no client-side path — the flow was designed to route image bytes through the server, which adds unnecessary bandwidth and latency. There was also no UI for users to upload a custom avatar photo.
Solution
Use pushduck to wire up a presigned-URL upload flow. Image bytes go directly from the client to Cloudflare R2 — the server only issues the presigned URL and runs post-processing after the upload completes.
What changed
Server (
packages/happy-server)sources/app/api/routes/uploadRoutes.ts(new)cloudflareR2provider fromCLOUDFLARE_R2_*env varsavatarUploadroute:Authorization: Bearer <token>from the Web Request and callsauth.verifyToken()— auth runs inside pushduck middleware so structured JSON errors are returned on failurepublic/users/{userId}/avatars/{random12}.{ext}onUploadComplete: fetches the uploaded image from R2 → Sharp (thumbhash + dimensions) → single DB transaction (remove old avatar file records, insert newUploadedFile, updateAccount.avatar) → emits real-timeupdate-accountsocket event so all connected clients refresh immediatelysources/app/api/api.ts(modified)uploadRoutesatGET /v1/uploadandPOST /v1/upload.env.dev(modified)CLOUDFLARE_R2_*env vars with setup linkApp (
packages/happy-app)sources/sync/apiUpload.ts(new)Plain async
uploadAvatar(credentials, asset)— React Native compatible:POST /v1/upload?route=avatarUpload&action=presign→ presigned URL + keyfetch(asset.uri).blob()→PUTdirectly to R2POST /v1/upload?route=avatarUpload&action=complete→ triggersonUploadCompletesources/app/(app)/settings/account.tsx(modified)uploadAvatar, thensync.refreshProfile()to pick up the socket updateSetup
To enable uploads in a self-hosted deployment, set these env vars in
happy-server:Create an R2 bucket and API token at https://developers.cloudflare.com/r2/api/s3/tokens/ (Object Read & Write permissions).
Test plan
/v1/upload