Avatar and image management
Arbitex provides a full avatar management pipeline for user profile images. This guide explains how avatars are validated, processed, stored, and served — including how identity provider (IdP) photos from SCIM and OIDC sources are synchronized and how administrators control avatar state.
Introduction
Section titled “Introduction”Every Arbitex user account can have a profile avatar. Avatars appear in the following locations:
- User profile page — displayed next to the user’s display name and email
- Chat interface — shown alongside messages in conversation threads
- Admin user list — visible in the Users panel for quick identification
- Audit log entries — associated with events attributed to a user
Arbitex stores avatars using a content-addressed strategy: each image file is named by the SHA-256 hash of its binary content. This means identical images share a single stored file, URLs are permanently stable (the content never changes at a given URL), and cache headers can safely be set to immutable.
Avatars are always stored and served as WebP, regardless of the original upload format. This keeps file sizes small and serving consistent.
Upload flow
Section titled “Upload flow”When a user uploads a new avatar, the image passes through a four-stage pipeline before being stored.
[User uploads file] | v [1. Validate] ── reject if invalid format, too large, or malformed | v [2. Resize] ── scale to fit 256×256px, center-crop if non-square | v [3. Compress] ── convert to WebP at quality=80 | v [4. Store] ── write {sha256(bytes)}.webp to AVATAR_STORAGE_PATH | v [Update user record with new avatar hash]Stage 1: Validation
Section titled “Stage 1: Validation”The server checks the uploaded file before any processing occurs:
| Check | Constraint | Rejection reason |
|---|---|---|
| File format | JPEG, PNG, GIF, or WebP only | invalid_format |
| File size | Maximum 5 MB (configurable via AVATAR_MAX_SIZE_MB) |
file_too_large |
| Image dimensions | Must be at least 16×16 px | image_too_small |
| Parseable image | Must decode as a valid image | corrupt_image |
If any check fails, the server returns 400 Bad Request with a machine-readable error_code field and a human-readable message. The upload is rejected before any resize or storage operation takes place.
{ "error": "validation_failed", "error_code": "file_too_large", "message": "Avatar file exceeds the maximum allowed size of 5 MB."}Stage 2: Resize
Section titled “Stage 2: Resize”After validation passes, the image is resized to fit within the configured maximum dimension (default: 256×256 px).
Scaling rules:
- If the image is smaller than 256×256, it is upscaled to exactly 256×256.
- If the image is already square and larger than 256×256, it is downscaled to 256×256.
- If the image is non-square (e.g., 800×400), it is center-cropped to a square before being scaled to 256×256.
Center-crop behavior takes the largest centered square region from the image. For a 1200×600 image, this means a 600×600 region is cropped from the horizontal center, then scaled to 256×256.
Stage 3: Compression
Section titled “Stage 3: Compression”The resized image is converted to WebP format at quality=80. This produces files typically in the 5–15 KB range for photographic content and 2–8 KB for illustrations or flat graphics.
WebP is used because:
- It is widely supported by all modern browsers and HTTP clients
- It produces smaller files than JPEG at equivalent visual quality for avatars
- It provides a consistent format for serving, eliminating format negotiation
The quality setting (80) balances perceptual quality with file size. At 256×256 pixels, quality=80 is visually lossless for typical avatar content.
Stage 4: Storage
Section titled “Stage 4: Storage”Once compression completes, the server computes the SHA-256 hash of the compressed WebP bytes and writes the file to:
{AVATAR_STORAGE_PATH}/{sha256_hex}.webpIf a file with that hash already exists (because the same image was uploaded before, or by another user), the write is skipped — the existing file is reused. The user’s record is updated to point to the new hash regardless.
After the file is stored, the user’s avatar_hash field is updated and avatar_source is set to "upload".
Content-addressed filenames
Section titled “Content-addressed filenames”Content-addressed storage assigns each avatar a filename derived from the SHA-256 hash of its binary content, rather than from a user ID, timestamp, or sequential counter.
How it works
Section titled “How it works”- The compressed WebP bytes are hashed using SHA-256.
- The hex digest (64 characters) becomes the filename.
- The file is stored at
{AVATAR_STORAGE_PATH}/{hex}.webp.
Example filename:
a3f8c2e1d4b5609f3a87c2e1d4b56092e1d4b560a3f8c2e1d4b5609f3a87c2e.webpThe avatar URL exposed to clients follows the same structure:
/api/avatars/a3f8c2e1d4b5609f3a87c2e1d4b56092e1d4b560a3f8c2e1d4b5609f3a87c2e.webpBenefits
Section titled “Benefits”Immutable URLs. Because the filename is derived from the content, the file at a given URL never changes. A browser or CDN that caches the response can hold it indefinitely — the Cache-Control: immutable directive is safe to apply.
Automatic deduplication. Two users uploading identical images produce the same hash and share one file on disk. No additional deduplication logic is required.
No filename collisions. A user replacing their avatar generates a different hash (new content) and therefore a new filename. The old file remains on disk until the orphan cleanup job removes it. There is no possibility of one user’s upload overwriting another’s.
Tamper evidence. If a stored file is modified on disk, its content will no longer match the hash in its filename. Arbitex does not currently verify hashes on read, but the naming convention supports future integrity checks.
Serving avatars
Section titled “Serving avatars”Avatars are served by the platform API over a stable URL pattern.
Endpoint
Section titled “Endpoint”GET /api/avatars/{hash}.webpPath parameter:
| Parameter | Description |
|---|---|
hash |
The 64-character lowercase hex SHA-256 digest of the WebP content |
Example request:
GET /api/avatars/a3f8c2e1d4b5609f3a87c2e1d4b56092e1d4b560a3f8c2e1d4b5609f3a87c2e.webpSuccess response (200 OK):
HTTP/1.1 200 OKContent-Type: image/webpCache-Control: public, max-age=31536000, immutableContent-Length: 8432ETag: "a3f8c2e1d4b5609f3a87c2e1d4b56092e1d4b560a3f8c2e1d4b5609f3a87c2e"
[WebP image bytes]Not found response (404):
HTTP/1.1 404 Not FoundContent-Type: application/json
{ "error": "avatar_not_found", "message": "No avatar found for the specified hash."}A 404 is returned when the hash does not exist in storage. This occurs if the file was removed by the orphan cleanup job after the user’s record was deleted, or if the hash is malformed.
Cache headers
Section titled “Cache headers”The response includes Cache-Control: public, max-age=31536000, immutable. This instructs clients and intermediate caches to store the response for one year without revalidation. Because content-addressed filenames guarantee that the file at a given URL never changes, this is always safe.
Authentication
Section titled “Authentication”Avatar URLs are publicly accessible without authentication. This is intentional — avatars are non-sensitive profile images and restricting them would require all API clients and chat UIs to handle authenticated image requests. If your deployment requires access control on avatars, place the platform behind a reverse proxy that adds authentication at the edge.
CDN considerations
Section titled “CDN considerations”Arbitex avatar URLs are CDN-friendly by design:
- Origin pull: Configure your CDN to pull from
/api/avatars/on the platform origin. TheCache-Control: immutableheader ensures the CDN caches aggressively. - Edge caching: Once cached at the edge, avatar requests do not reach the origin. This reduces load on the platform for high-traffic deployments.
- Cache invalidation: Because filenames change when content changes, you do not need to invalidate CDN cache entries when a user updates their avatar. The old URL becomes unused; the new URL is served fresh on first request.
- CORS: If your CDN or frontend origin differs from the platform API origin, ensure the platform’s CORS configuration allows
GETrequests from your frontend domains.
IdP photo sync
Section titled “IdP photo sync”Arbitex can automatically populate user avatars from identity provider sources. Photos sourced from the IdP pass through the same validate → resize → compress → store pipeline as direct uploads.
SCIM provisioning photos
Section titled “SCIM provisioning photos”When SCIM provisioning is configured, the SCIM User schema includes a photos attribute. If your IdP populates this attribute, Arbitex downloads and processes the photo during user creation and on subsequent SCIM update events.
SCIM User schema excerpt:
{ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "id": "user-uuid", "displayName": "Alice Chen", "photos": [ { "value": "https://idp.example.com/photos/alice.jpg", "type": "photo", "primary": true } ]}When Arbitex receives a SCIM POST /scim/v2/Users or PUT /scim/v2/Users/{id} request containing a photos array, it:
- Selects the entry with
"primary": true, or the first entry if none is marked primary. - Downloads the image from the URL in
value. - Runs the full validation, resize, and compression pipeline.
- Stores the result and sets
avatar_source: "scim"on the user record.
If the download fails (network error, 4xx/5xx from the IdP), the error is logged and avatar sync is skipped for that event. The SCIM provisioning response still returns success — avatar sync failure does not fail the provisioning operation.
OIDC picture claim
Section titled “OIDC picture claim”When OIDC SSO is configured, the ID token and userinfo endpoint may include a picture claim containing a URL to the user’s photo.
ID token excerpt:
{ "sub": "user-uuid", "name": "Alice Chen", "picture": "https://lh3.googleusercontent.com/a/photo-url"}On first login via OIDC, Arbitex downloads and processes the picture URL using the same pipeline. On subsequent logins, the picture URL is checked against the stored value — if it has changed, the new photo is downloaded and processed.
avatar_source is set to "oidc" when a photo is successfully synced from the OIDC picture claim.
Avatar source field
Section titled “Avatar source field”Every user record includes an avatar_source field that tracks where the current avatar originated.
| Value | Description |
|---|---|
"upload" |
The user uploaded their own image directly |
"scim" |
The avatar was synced from the SCIM photos attribute |
"oidc" |
The avatar was synced from the OIDC picture claim |
"default" |
No avatar is set; the UI renders a system-generated initials placeholder |
The avatar_source field appears in the user detail response from the admin API:
{ "id": "user-uuid", "display_name": "Alice Chen", "avatar_hash": "a3f8c2e1d4b5609f3a87c2e1d4b56092e1d4b560a3f8c2e1d4b5609f3a87c2e", "avatar_source": "oidc", "created_at": "2025-11-01T09:00:00Z"}When avatar_source is "default", avatar_hash is null.
Precedence rules
Section titled “Precedence rules”When multiple avatar sources are available, Arbitex applies the following precedence:
user upload > IdP photo (SCIM or OIDC) > defaultUser upload takes highest precedence. Once a user uploads their own avatar, avatar_source changes to "upload". Subsequent SCIM updates and OIDC logins do not overwrite the user-uploaded avatar, even if the IdP photo changes.
IdP photo takes precedence over default. If no user upload exists, any available SCIM or OIDC photo is used. If both SCIM and OIDC photos are available, SCIM takes precedence (SCIM provisioning events are processed independently of login, and SCIM is considered the authoritative directory source).
Default is the fallback. If neither a user upload nor an IdP photo is available, the UI renders a placeholder based on the user’s initials. No file is stored; avatar_hash is null.
An administrator can reset a user’s avatar from "upload" back to IdP or default using the admin DELETE endpoint described in the next section. This re-enables IdP photo sync for the user.
Admin controls
Section titled “Admin controls”Administrators manage avatar state through the admin API. The Cloud Portal admin panel surfaces these controls in the user detail view.
Remove a user’s custom avatar
Section titled “Remove a user’s custom avatar”DELETE /api/v1/admin/users/{id}/avatarAuthorization: Bearer <admin-token>Path parameter:
| Parameter | Description |
|---|---|
id |
The UUID of the target user |
Success response (204 No Content):
The user’s avatar_hash is cleared and avatar_source reverts according to the following logic:
- If the user has a SCIM photo URL on record, avatar sync is triggered immediately and
avatar_sourcebecomes"scim". - If no SCIM photo is available but an OIDC
pictureclaim was recorded at last login, avatar sync is triggered andavatar_sourcebecomes"oidc". - If neither IdP source is available,
avatar_hashis set tonullandavatar_sourcebecomes"default".
Error responses:
| Status | Condition |
|---|---|
404 Not Found |
No user exists with the given ID |
409 Conflict |
The user’s current avatar_source is not "upload" — there is no custom avatar to remove |
Example:
curl -s -X DELETE \ "https://platform.example.com/api/v1/admin/users/user-uuid/avatar" \ -H "Authorization: Bearer ${ADMIN_TOKEN}"Viewing avatar status in the admin panel
Section titled “Viewing avatar status in the admin panel”The user detail page in the Cloud Portal (Admin → Users → select user) shows:
- Avatar preview — the current avatar image, or the initials placeholder
- Avatar source — one of
upload,scim,oidc, ordefault - Avatar hash — the hex digest (truncated to 12 characters with a copy button for the full value)
- Remove avatar button — visible only when
avatar_sourceis"upload"
Clicking Remove avatar calls the DELETE endpoint and refreshes the panel. If an IdP photo is available, the new IdP-sourced avatar appears immediately.
Bulk operations
Section titled “Bulk operations”Bulk avatar removal is not supported. The DELETE endpoint is per-user only. If you need to reset avatars for a large number of users (for example, following an IdP migration), use the admin API in a loop:
# Reset avatars for a list of user IDswhile IFS= read -r user_id; do curl -s -X DELETE \ "https://platform.example.com/api/v1/admin/users/${user_id}/avatar" \ -H "Authorization: Bearer ${ADMIN_TOKEN}" \ -o /dev/null -w "${user_id}: %{http_code}\n"done < user_ids.txtOrphan cleanup
Section titled “Orphan cleanup”An orphaned avatar file is a WebP on disk whose hash is no longer referenced by any user record. Orphans are created when:
- A user deletes their account (user record removed, file remains)
- A user uploads a new avatar (old hash removed from user record, old file remains)
- An admin removes a custom avatar that had no IdP fallback (old hash cleared, old file remains)
Because avatar files are small (typically 5–15 KB each), orphans accumulate slowly. However, in deployments with high user churn or frequent avatar updates, orphans can build up over time.
Cleanup job
Section titled “Cleanup job”Arbitex runs a scheduled orphan cleanup task at the interval configured by AVATAR_CLEANUP_INTERVAL (default: 24 hours). The job:
- Lists all
.webpfiles inAVATAR_STORAGE_PATH. - Queries the database for all non-null
avatar_hashvalues across all user records. - Computes the set difference: files on disk that have no corresponding hash in the database.
- Deletes each orphaned file.
- Logs a summary of the cleanup run.
Example log output:
[avatar-cleanup] Starting orphan cleanup scan[avatar-cleanup] Found 1,247 files on disk[avatar-cleanup] Found 1,183 referenced hashes in database[avatar-cleanup] Identified 64 orphaned files[avatar-cleanup] Deleted 64 orphaned files (total: 892 KB reclaimed)[avatar-cleanup] Cleanup completed in 0.42sSafety guarantees
Section titled “Safety guarantees”The cleanup job only deletes files that match all of the following conditions:
- The file extension is
.webp(non-WebP files in the storage directory are never touched) - The filename (without extension) matches the expected 64-character hex pattern
- The filename does not appear in the set of active
avatar_hashvalues from the database
The query for active hashes is performed immediately before the delete pass. Files created between the scan and the delete pass (new uploads during the cleanup window) are safe — they are referenced in the database by the time the delete pass runs.
Configuring the cleanup interval
Section titled “Configuring the cleanup interval”Set AVATAR_CLEANUP_INTERVAL in your environment configuration. The value is a duration string:
# Run cleanup every 12 hoursAVATAR_CLEANUP_INTERVAL: 12h
# Run cleanup every 6 hours (high-churn deployments)AVATAR_CLEANUP_INTERVAL: 6h
# Disable cleanup (not recommended for production)AVATAR_CLEANUP_INTERVAL: 0Setting the interval to 0 disables the scheduled job entirely. If you disable automatic cleanup, you are responsible for periodically removing orphaned files to prevent unbounded storage growth.
Docker volume configuration
Section titled “Docker volume configuration”Avatar files are stored on a persistent volume. The default storage path inside the container is /data/avatars/. This directory must survive container restarts and, in multi-replica deployments, must be shared across all replica instances.
Docker Compose
Section titled “Docker Compose”Mount a named volume at the default storage path:
services: platform: image: ghcr.io/arbitex/platform:latest environment: AVATAR_STORAGE_PATH: /data/avatars volumes: - avatar_data:/data/avatars # ... other configuration
volumes: avatar_data: driver: localFor a custom storage path:
services: platform: image: ghcr.io/arbitex/platform:latest environment: AVATAR_STORAGE_PATH: /opt/arbitex/avatars volumes: - avatar_data:/opt/arbitex/avatars
volumes: avatar_data: driver: localKubernetes PersistentVolumeClaim
Section titled “Kubernetes PersistentVolumeClaim”For Kubernetes deployments, create a PVC and mount it into the platform pod:
apiVersion: v1kind: PersistentVolumeClaimmetadata: name: arbitex-avatars namespace: arbitexspec: accessModes: - ReadWriteOnce resources: requests: storage: 5Gi storageClassName: standard# platform-deployment.yaml (excerpt)apiVersion: apps/v1kind: Deploymentmetadata: name: arbitex-platform namespace: arbitexspec: template: spec: containers: - name: platform image: ghcr.io/arbitex/platform:latest env: - name: AVATAR_STORAGE_PATH value: /data/avatars volumeMounts: - name: avatar-storage mountPath: /data/avatars volumes: - name: avatar-storage persistentVolumeClaim: claimName: arbitex-avatarsContainer permissions
Section titled “Container permissions”The platform container runs as a non-root user. The storage directory must be writable by the container’s UID. If you are using a pre-existing volume or host mount, set permissions before starting the container:
# Set ownership to UID 1000 (platform container user)chown -R 1000:1000 /path/to/avatar/storagechmod -R 750 /path/to/avatar/storageIf using a Kubernetes init container:
initContainers: - name: volume-permissions image: busybox:1.36 command: ["sh", "-c", "chown -R 1000:1000 /data/avatars && chmod -R 750 /data/avatars"] volumeMounts: - name: avatar-storage mountPath: /data/avatarsMulti-replica storage
Section titled “Multi-replica storage”Single-node ReadWriteOnce volumes are not suitable for deployments with more than one platform replica, because only one pod can mount the volume at a time. For multi-replica configurations, use shared network storage:
| Storage type | Kubernetes access mode | Notes |
|---|---|---|
| NFS | ReadWriteMany |
Supported; ensure NFS server is highly available |
| Azure Files | ReadWriteMany |
Supported via Azure CSI driver |
| AWS EFS | ReadWriteMany |
Supported via EFS CSI driver |
| GCP Filestore | ReadWriteMany |
Supported via Filestore CSI driver |
| S3 / Blob storage | N/A | Not currently supported as a native backend |
S3-compatible object storage as a native avatar backend is planned for a future release. Until then, shared filesystem storage is the recommended approach for multi-replica Kubernetes deployments.
Backup considerations
Section titled “Backup considerations”Avatar storage volumes are small relative to other platform data. A deployment with 1,000 users with avatars will typically have 5–15 MB of avatar data on disk (after deduplication). Even at 100,000 users, the volume is unlikely to exceed a few hundred megabytes.
Include the avatar volume in your backup rotation alongside the database:
# Backup avatar storage to a tarballtar -czf avatars-backup-$(date +%Y%m%d).tar.gz -C /data/avatars .
# Or rsync to a backup locationrsync -av --delete /data/avatars/ backup-host:/backups/avatars/Because avatar filenames are content-addressed and immutable, incremental backups (rsync, deduplicating backup tools) work efficiently — unchanged files are never re-copied.
Configuration reference
Section titled “Configuration reference”All avatar-related configuration is provided as environment variables. Set these in your Docker Compose environment block, Kubernetes ConfigMap, or deployment secrets manager.
| Variable | Type | Default | Description |
|---|---|---|---|
AVATAR_STORAGE_PATH |
string | /data/avatars |
Absolute filesystem path where WebP avatar files are stored |
AVATAR_MAX_SIZE_MB |
integer | 5 |
Maximum upload size in megabytes. Uploads exceeding this limit are rejected during validation. |
AVATAR_MAX_DIMENSION |
integer | 256 |
Maximum width and height in pixels. Images larger than this are resized before compression. |
AVATAR_WEBP_QUALITY |
integer | 80 |
WebP compression quality, 1–100. Higher values produce larger files with better visual quality. |
AVATAR_CLEANUP_INTERVAL |
duration | 24h |
How often the orphan cleanup job runs. Set to 0 to disable. Accepts Go duration strings: 1h, 30m, 12h. |
AVATAR_IDP_SYNC_ENABLED |
boolean | true |
Enable or disable IdP photo sync from SCIM and OIDC sources. When false, user photos attributes and picture claims are ignored. |
Example configuration block (Docker Compose):
environment: AVATAR_STORAGE_PATH: /data/avatars AVATAR_MAX_SIZE_MB: "5" AVATAR_MAX_DIMENSION: "256" AVATAR_WEBP_QUALITY: "80" AVATAR_CLEANUP_INTERVAL: 24h AVATAR_IDP_SYNC_ENABLED: "true"Example ConfigMap (Kubernetes):
apiVersion: v1kind: ConfigMapmetadata: name: arbitex-platform-config namespace: arbitexdata: AVATAR_STORAGE_PATH: /data/avatars AVATAR_MAX_SIZE_MB: "5" AVATAR_MAX_DIMENSION: "256" AVATAR_WEBP_QUALITY: "80" AVATAR_CLEANUP_INTERVAL: 24h AVATAR_IDP_SYNC_ENABLED: "true"Troubleshooting
Section titled “Troubleshooting”Avatar not updating after upload
Section titled “Avatar not updating after upload”Symptom: The user uploads a new avatar through the UI, the upload appears to succeed, but the old avatar continues to appear.
Likely causes and remedies:
-
Browser cache. The old avatar URL may be cached by the browser. Because avatar URLs carry
Cache-Control: immutable, the browser will not re-request the old URL. The new avatar has a different URL (different hash). Check whether the new avatar URL is being requested in the browser’s network panel. A hard refresh (Ctrl+Shift+R) will not help — the URL must change for the new image to load. -
CDN serving stale content. If a CDN is in front of the platform, verify that the new avatar URL reaches the CDN. Because filenames change on upload, the CDN should not have a cached entry for the new URL. If the old URL is still being used in the UI, the issue is in the frontend — the avatar URL is not being refreshed after upload.
-
Upload validation failure not surfaced. The upload may have been rejected silently. Check the platform API logs for
validation_failedorfile_too_largeevents associated with the user’s ID and the time of the upload attempt. -
Storage write failure. Check that
AVATAR_STORAGE_PATHis writable by the container user and that the volume has sufficient free space.
IdP photo not syncing
Section titled “IdP photo not syncing”Symptom: A user’s SCIM or OIDC photo is not appearing as their avatar.
Check for SCIM:
- Confirm that the SCIM User object sent by your IdP includes a
photosarray with aprimary: trueentry. - Check the platform logs for SCIM update events for the user. Look for
avatar_sync_failedlog entries that include a download URL and HTTP status code. - Confirm that the platform host can reach the photo URL. SCIM photo URLs must be publicly accessible or reachable from the platform’s network.
- Verify that
AVATAR_IDP_SYNC_ENABLEDistrue.
Check for OIDC:
- Confirm that the ID token or userinfo response from your IdP includes a
pictureclaim. - For Google Workspace: the
pictureclaim is present by default when theprofileOAuth scope is requested. - For Microsoft Entra ID: the
pictureclaim is not included by default. Entra ID requires additional configuration to return a photo URL in the OIDC flow; check your Entra ID app manifest. - Check the platform logs for
oidc_avatar_sync_failedentries at login time. - Confirm that
AVATAR_IDP_SYNC_ENABLEDistrue.
If the user has a custom upload: A user’s own upload (avatar_source: "upload") takes precedence over IdP photos. If the user has previously uploaded an avatar, IdP sync will not overwrite it. An admin must use the DELETE endpoint to reset avatar_source before IdP sync will apply.
Storage growing unexpectedly
Section titled “Storage growing unexpectedly”Symptom: The avatar storage volume is larger than expected given the number of users.
Likely causes:
-
Orphan cleanup is disabled or not running. Check that
AVATAR_CLEANUP_INTERVALis not set to0. Check platform logs for[avatar-cleanup]entries to confirm the job is running on schedule. -
High avatar churn. If users frequently update their avatars, each update leaves behind an orphaned file until the next cleanup run. Reduce
AVATAR_CLEANUP_INTERVALto run cleanup more frequently. -
Non-avatar files in the storage directory. The cleanup job only removes files matching the 64-character hex
.webppattern. If other files have accumulated inAVATAR_STORAGE_PATH, they will not be removed automatically. Inspect the directory and remove unexpected files manually.
To immediately inspect storage usage:
# Count files and total sizefind /data/avatars -name "*.webp" | wc -ldu -sh /data/avatars/Avatar returns 404
Section titled “Avatar returns 404”Symptom: A request to /api/avatars/{hash}.webp returns 404, but the user’s record shows a non-null avatar_hash.
Likely causes:
-
File was cleaned up after the user record was deleted. If the user was recently deleted and the orphan cleanup ran, the file may have been removed. This is expected behavior. Re-upload is not possible for a deleted user.
-
Storage volume not mounted. If the platform container was restarted without the persistent volume,
AVATAR_STORAGE_PATHmay be an empty ephemeral directory. Verify the volume mount is configured and the directory contains the expected files. -
Hash mismatch. If files in storage were renamed or moved, the hash in the user record no longer matches a filename on disk. Do not rename avatar files manually. Restore from backup if files have been reorganized.
-
Wrong
AVATAR_STORAGE_PATH. If the platform was restarted with a differentAVATAR_STORAGE_PATHthan the one used when the file was written, the platform is looking in the wrong directory. Ensure the environment variable is consistent across restarts and replicas.
To verify a specific file’s presence on disk:
HASH="a3f8c2e1d4b5609f3a87c2e1d4b56092e1d4b560a3f8c2e1d4b5609f3a87c2e"ls -lh "/data/avatars/${HASH}.webp"If the file is missing and the user account is active, re-uploading an avatar resolves the issue immediately.