Skip to main content

Images Module

Location: backend/src/images/

The images module provides image upload and retrieval services for the application. It handles both standard (backend-mediated) and resumable (TUS protocol) uploads to Supabase Storage. The module has no controller of its own - its ImagesService is imported by AuthModule and used by the AuthController for avatar management.

Files

FilePurpose
images.module.tsModule definition; provides and exports ImagesService
images.service.tsCore image upload, download, and storage logic
transformer.tsResponse shaping for API versioning (images.uploaded, images.resumable)
dto/create-resumable-upload.dto.tsValidation for resumable upload initialization
dto/complete-upload.dto.tsValidation for resumable upload completion

Architecture

AuthController (auth/auth.controller.ts)

├── GET /auth/avatar → ImagesService.getImageFromUserProfile()
├── POST /auth/avatar → ImagesService.setImageToUserProfile()
├── POST /auth/avatar/resumable → ImagesService.createResumableUpload()
└── POST /auth/avatar/complete → ImagesService.completeResumableUpload()

The ImagesModule exports ImagesService and is imported by AuthModule. This keeps avatar endpoints grouped under /auth while the image logic remains in a dedicated, reusable module.

Upload Methods

Standard Upload (Small Files)

For files under 5MB. The entire file passes through the backend.

  1. Client sends multipart/form-data to POST /auth/avatar
  2. Backend reads the file buffer, validates size and MIME type
  3. Uploads to Supabase Storage (images bucket) with upsert: true
  4. Generates a cache-busted public URL (?t={timestamp})
  5. Updates user_profile.avatar_url in the database
  6. Updates the avatar and profile caches

Resumable Upload (TUS Protocol)

For larger files or unreliable connections. The file uploads directly from the client to Supabase - no data passes through the backend.

Flow:

  1. Client calls POST /auth/avatar/resumable with file metadata
  2. Backend validates size/type, creates a signed upload URL via Supabase
  3. Returns TUS endpoint, token, headers, and metadata
  4. Client uses tus-js-client or Uppy to upload directly to Supabase
  5. Client calls POST /auth/avatar/complete with the storage path
  6. Backend verifies the file exists and updates the profile

Validation

ConstraintValue
Max file size5 MB
Allowed typesimage/jpeg, image/png, image/webp
Storage bucketimages
Default pathavatars/{userId}.{ext}
TUS chunk size6 MB (Supabase requirement)

Cache-Busting

Since the storage path is deterministic (avatars/{userId}.jpg), re-uploading produces the same URL. A ?t={timestamp} query parameter is appended to force browsers and CDNs to fetch the new image instead of serving the cached version.

The extractStoragePath() helper strips this query param when downloading so the Supabase Storage lookup isn't affected.

Caching Strategy

Cache KeyTTLUpdated When
avatar:{userId}1 hourSet after each upload; checked before DB query on GET /avatar
profile:{userId}30 daysPatched in-place with new avatar_url via CacheService.update()

On GET /avatar, the service checks avatar:{userId} first. On cache miss, it queries the database and populates the cache. On upload, both caches are updated atomically.

Custom Storage Paths

Both upload methods accept an optional pathname parameter to override the default avatars/ directory:

POST /auth/avatar?pathname=profiles
→ stores at: profiles/{userId}.jpg

POST /auth/avatar/resumable
{ "pathname": "documents" }
→ stores at: documents/{userId}.png

Dependencies

ServicePurpose
SupabaseServiceSupabase client for storage and database operations
CacheServiceAvatar URL and profile caching
ConfigServiceReads SUPABASE_URL to construct the TUS endpoint