ProjectR Docs
Backend (Convex)

Storage (R2)

Upload sessions, signed URLs, and size gates.

Media storage is Cloudflare R2 via @convex-dev/r2, wrapped by convex/storage/*.

Upload flow

  1. generateUploadUrl (public mutation) — auth required. category is required and triggers staff-only gating for managed categories, MIME validation, and size checks; a signed PUT is minted and an r2UploadSessions ownership row is written in the same transaction.
    • All arguments are optional on purpose: the backend deploys before the dashboard, and a required arg would break every running dashboard build for the deploy window. Undeclared calls get legacy behavior plus an ownership row.
  2. The client PUTs bytes, then calls syncMetadata; the component HEADs the object and invokes onSyncMetadata.
  3. The truth gate is onSyncMetadata — a scheduled action with no auth context (ctx.auth is always null). It compares the real HEAD content-type/size against the category cap and, on violation, deletes the object (unless R2_ENFORCE_UPLOAD_LIMITS=false), clears profile references, and deletes the mint record. Any new logic here must authorize via the mint record, never ctx.auth.

Limits

CategoryMax size
avatar / avatarCatalog5 MB
review / campaign / productGroup10 MB
carousel image1 MB
carousel gif10 MB
carousel video250 MB
general50 MB (*/* until no undeclared uploads remain)
export100 MB

Signed URLs and deletion

getFileUrl clamps expiresIn to 60–3600 s (default 900); the plural getFileUrls applies only the 900 s default with no clamp. deleteFile lets staff delete any managed asset; regular users only keys their profile currently references. Avatar updates authorize against the mint record.

Purge cron (daily 05:00 UTC) drops mint rows older than 7 days — after the first upload the profile row itself is the ownership proof. Mobile uploads nothing directly (avatars come from the managed avatarCatalog by slug); the dashboard is the primary uploader.

On this page