Profile Avatars
A signed-in user manages their own profile picture. There is no user id in either path: the account is always the caller's, so one user can never write another's avatar.
The Two Endpoints
| Endpoint | Behavior |
|---|---|
POST /api/account/avatar |
Multipart upload under the field name file. Returns 201 with the stored image URL. |
DELETE /api/account/avatar |
Clears the picture and removes the stored object. Returns 200, and is idempotent. |
curl -X POST https://app.example.com/api/account/avatar \
-H "Cookie: $SESSION" \
-F "file=@portrait.png"{
"success": true,
"image": "/api/buckets/avatars/files/9f3c1e2a-7b44-4d10-9e21-8a6f0c1d2e3b-avatar.png"
}The URL lands on the user's image, which is what the user directory projects and what GET /api/account/export reports.
Declare an avatars Bucket
This is a hard requirement, not a convention:
buckets:
- name: avatars
public: true
maxFileSize: 2097152Without a bucket named avatars, upload answers 404. The URL this endpoint mints names avatars literally, so there is deliberately no fallback to the implicit default bucket — a fallback would store the file, hand back a URL, and leave the user with an avatar that never loads. The name is checked before anything is read.
public: true is the usual choice. An avatar is rendered into other people's browsers by definition, so a private bucket makes every <img> tag 404 for everyone but its owner.
maxFileSize on that bucket caps the upload; without it the limit is 2 MB. Over the cap answers 413.
What Is Accepted
Sovrium ignores the filename you send and ignores the Content-Type you declare. It decodes the bytes and reads the real container header, then derives the extension from that.
| Format | Stored as |
|---|---|
| PNG | .png |
| JPEG | .jpg |
| WebP | .webp |
Anything else — including a file that merely claims to be one of the three — answers 400 and leaves the existing avatar untouched.
Discarding the filename removes a whole class of problem rather than checking for it. The filename is the one part of an upload entirely chosen by the uploader, so path traversal, null bytes and extension/content mismatch stop being checks that could be wrong and become shapes that cannot arrive. Reading the container header rather than the declared type means HTML calling itself image/png is refused here, instead of being stored and later served from your own origin.
The Image URL Is Not Writable
image cannot be set by hand. Sending one to POST /api/auth/update-user — or to sign-up, or to the admin user endpoints — answers 400:
{
"message": "A profile image cannot be set directly. Upload one through the account avatar endpoint, or send `image: null` to clear it."
}Every non-null value is refused, including a well-formed URL pointing at your own avatars bucket. Shape would prove the URL points at this instance; it would not prove the object belongs to the caller, and that is the thing worth proving. Sending image: null is permitted and clears the picture; omitting the field entirely leaves it alone, so an ordinary name change still works.
Outcomes
| Outcome | Status |
|---|---|
| Uploaded | 201 with the image URL |
| Deleted, or already absent | 200 |
| No session | 401 |
No avatars bucket declared |
404 |
No file field in the body |
400 |
| Not a decodable PNG, JPEG or WebP | 400 |
| Over the bucket cap, or over 2 MB | 413 |
Deleting an account through GDPR erasure removes the stored avatar object along with the row.
Related Pages
- User Management — the directory that renders these images.
- Buckets Overview — declaring the
avatarsbucket. - File Operations — the download route the minted URL points at.
- GDPR & Privacy — what erasure removes.
Last updated September 1, 2026
This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta. Contributions and corrections are welcome.