Skip to main content
View as Markdown

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.
>_ terminal
curl -X POST https://app.example.com/api/account/avatar \
  -H "Cookie: $SESSION" \
  -F "file=@portrait.png"
app.json
{
  "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:

app.yaml
buckets:
  - name: avatars
    public: true
    maxFileSize: 2097152

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.

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:

app.json
{
  "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.

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.

Built with Sovrium