Avatar Storage Architecture
This document describes the technical implementation of profile picture (avatar) storage in CADENSA using Wasabi S3 object storage.
Overview
Profile pictures are stored in Wasabi S3 cloud object storage rather than the local filesystem or the database. The backend processes every uploaded image before storing it: it resizes it to 400×400 pixels, converts it to WebP format, and uploads the optimised file to the cadensa-avatars bucket.
Access to stored images is controlled via pre-signed URLs with a 7-day TTL. The raw Wasabi object key is stored in the database; the signed URL is generated on every API response.
Infrastructure
| Property | Value |
|---|---|
| Provider | Wasabi |
| Region | eu-central-2 |
| Bucket | cadensa-avatars |
| Object Lock | Disabled |
| Public access | ❌ No (access via pre-signed URLs only) |
| Signed URL TTL | 7 days |
Environment variables
WASABI_ACCESS_KEY_ID=...
WASABI_SECRET_ACCESS_KEY=...
WASABI_REGION=eu-central-2
WASABI_ENDPOINT=https://s3.eu-central-2.wasabisys.com
WASABI_BUCKET=cadensa-files # general-purpose bucket
WASABI_BUCKET_AVATARS=cadensa-avatars
Upload Pipeline
Client (browser/app)
│
│ POST /api/upload/avatar (multipart/form-data)
▼
multer (memoryStorage)
│ req.file.buffer ← image in RAM, never written to disk
▼
avatarOptimizer.ts
│ sharp: resize 400×400 cover crop
│ convert to WebP, quality 80
│ returns { buffer, contentType: 'image/webp', ext: '.webp', originalSize, optimizedSize }
▼
wasabi-s3.service.ts → uploadAvatar()
│ key: avatars/{userId}/{timestamp}.webp
│ bucket: cadensa-avatars
▼
Database
│ user.avatar = "wasabi:avatars/{userId}/{timestamp}.webp"
▼
API Response
│ url: "wasabi:avatars/..." (stored key, for DB)
│ signedUrl: "https://..." (pre-signed, 7 days)
Object Key Format
Object keys follow this pattern:
avatars/{userId}/{unixTimestampMs}.webp
Example:
avatars/6627a3f1e2b4c10012345678/1748000000000.webp
The value stored in user.avatar in MongoDB is prefixed with wasabi::
wasabi:avatars/6627a3f1e2b4c10012345678/1748000000000.webp
This prefix allows the backend to distinguish Wasabi keys from legacy data (e.g. Gravatar URLs, local filenames).
Image Optimisation
Implemented in cadensa-backend/src/services/upload/avatarOptimizer.ts using the sharp library.
| Parameter | Value |
|---|---|
| Dimensions | 400 × 400 px |
| Fit | cover (centre crop) |
| Format | WebP |
| Quality | 80% |
| Typical size reduction | ~95–97% vs. original JPEG/PNG |
// avatarOptimizer.ts (simplified)
export async function optimizeAvatar(buffer: Buffer, originalSize: number) {
const optimized = await sharp(buffer)
.resize(400, 400, { fit: 'cover', position: 'centre' })
.webp({ quality: 80 })
.toBuffer();
return {
buffer: optimized,
contentType: 'image/webp',
ext: '.webp',
originalSize,
optimizedSize: optimized.length,
};
}