Outcome
Upload a PNG, JPEG, or WebP image up to 2 MiB as the canonical workspace avatar.
Captures exercise real Auth/workspace handlers and canonical isolated SQL. Identity, Brand responses and the object store are simulated.
Before you start
-
Verify the pinned identity and exact app/organization/workspace from workspaces.list. Runtime authorization requires its workspace owner, organization owner/super_admin or verified platform authority; an ordinary organization admin who is not its owner is refused.
-
Resolve Brand choices through workspaces.avatar.options for this target; its Brand workspace is separate from the workspace whose avatar changes. Brand access and immutable logo publication require their own proof.
-
Retain the prior preset/Brand reference or original image before a mutation, review the exact plan and its compensation, and read canonical state after an unknown result before retrying.
-
Uploaded raster URLs are public to anyone holding the URL and permit a five-minute browser cache. Use an image suitable for public access; replacing/removing stored bytes cannot recall downloaded or cached copies.
-
Use a complete PNG, JPEG or WebP raster up to 2 MiB decoded. Match MIME and signature and omit a data-URL prefix. Signature validation does not prove full rendering or appropriate dimensions.
Discover the target
topolo whoami --json
topolo actions get app_topolo_auth.workspaces.avatar.upload --detail --json
After discovering the application, run topolo workspaces list <discovered-application> --json, replacing <discovered-application> with its exact published application id or slug.
Read workspaces.avatar.options with the exact workspaceId, app_id and org_id to discover accessible published Brand kit/asset choices.
Input fields
| Field | Required | Meaning |
|---|---|---|
appId | Yes | Canonical target application id returned by discovery, such as the application that owns this workspace; it is not automatically the Auth app id. |
organizationId | Yes | Authorized organization id for the target workspace. The request must satisfy the active organization or verified platform authority; it cannot select an arbitrary tenant. |
workspaceId | Yes | Existing exact-app workspace id from workspaces.list. This is the workspace whose avatar changes, not the Brand workspace used to resolve logo choices. |
fileName | Yes | Nonempty original-file label. The stored avatar is identified by an opaque id; its original filename is not returned or retained as an identity. |
contentBase64 | Yes | Raw standard base64 raster bytes, without a data-URL prefix. Decoded content may not exceed 2 MiB. Preserve the intended original image and avoid pasting private images into public logs. |
contentType | Yes | image/png, image/jpeg or image/webp; it must match the detected raster signature. Signature validation is not full image decoding or a dimensions check. |
Effects
-
Creates public raster bytes and writes their opaque URL to the exact authorized workspace.
-
Attempts new-object cleanup on SQL failure and deletes previous owned uploaded bytes after success; these are not one transaction.
Interpret the response
Workspace management is authorized for this exact app, organization and workspace. The workspace owner can manage its own workspace. Organization owner/super_admin and verified platform authority can manage others; an ordinary organization admin who is not the workspace owner is refused. Seating in the application is separate from this management authority.
workspace is the canonical stored workspace, including its exact id/appId/organizationId, owner ids, status, slug and timestamps. avatar.kind identifies preset, image or brand; preserve every other workspace. The returned updatedAt comes from SQL and need not equal the envelope timestamp.
An uploaded image is replaced and its old stored bytes are deleted. Brand selection stores the kit/asset/public URL reference; it does not clone Brand bytes or publish a kit. A preset stores only its published preset id.
The canonical write, old-image deletion and nonfatal audit are separate. A cleanup failure can return 500 after the new avatar is stored. Read back before considering a retry or compensation. Restoring an old image requires its original bytes and produces a new opaque URL; it does not recreate the previous image id or revoke downloaded/cached copies.
Uploads accept only nonempty filenames and correctly encoded base64 PNG, JPEG or WebP signatures up to 2 MiB decoded. SVG, mismatched MIME, invalid base64 and excessive bytes are refused. The handler checks signature/size, not whether the full raster renders or its dimensions are appropriate.
Store creation precedes canonical identity/workspace SQL. If the canonical write fails, the handler attempts to delete the newly uploaded object. Failure of that cleanup is a server error and can leave an unreferenced object. Success does not prove Cloudflare durability, downstream rendering or cache refresh; verify these independently.
Avatar browser URLs are public bearer-by-possession URLs. Their binary responses permit a five-minute public cache. Removing or replacing stored bytes cannot recall copies already cached or downloaded. Do not use this feature for confidential images.
Worked examples
All ids, public URLs and raster bytes below are synthetic local fixtures. Resolve authorized live targets and review the exact unchanged plan before execution. These raw target responses do not prove the live gateway, provider persistence, public rendering or cache invalidation.
Owner uploads a raster and stores its canonical workspace URL
Input · payload.json:
{
"appId": "app_topolo_web",
"organizationId": "org_avatar_fixture",
"workspaceId": "ws_avatar_fixture",
"fileName": "fixture-pixel.png",
"contentType": "image/png",
"contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a8ioAAAAASUVORK5CYII="
}
CLI:
topolo actions validate app_topolo_auth.workspaces.avatar.upload --data @payload.json --json
topolo actions plan app_topolo_auth.workspaces.avatar.upload --data @payload.json --confirm --json
topolo actions call app_topolo_auth.workspaces.avatar.upload --data @payload.json --confirm --detail --json
MCP call after topolo_validate_action and topolo_plan_action on this same input:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_auth.workspaces.avatar.upload",
"input": {
"appId": "app_topolo_web",
"organizationId": "org_avatar_fixture",
"workspaceId": "ws_avatar_fixture",
"fileName": "fixture-pixel.png",
"contentType": "image/png",
"contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a8ioAAAAASUVORK5CYII="
},
"confirm": true,
"detail": true
}
}
Captured local handler response · HTTP 200:
{
"success": true,
"message": "Workspace avatar updated",
"data": {
"workspace": {
"id": "ws_avatar_fixture",
"appId": "app_topolo_web",
"organizationId": "org_avatar_fixture",
"name": "Creative",
"avatar": {
"kind": "image",
"url": "https://auth.topolo.dev/api/avatars/00000000-0000-4000-8000-000000000001"
},
"slug": "creative",
"status": "active",
"isDefault": false,
"ownerUserId": "usr_avatar_owner",
"createdByUserId": null,
"createdAt": 1,
"updatedAt": 1790980947,
"archivedAt": null
}
},
"timestamp": "2026-10-03T00:00:00.000Z"
}
File upload convenience · use the same reviewed bytes and published upload contract:
{
"appId": "app_topolo_web",
"organizationId": "org_avatar_fixture",
"workspaceId": "ws_avatar_fixture"
}
Save this scope as scope.json; the CLI derives fileName/contentBase64 from the reviewed local file. Validate and plan its exact derived payload before confirming:
topolo actions upload app_topolo_auth.workspaces.avatar.upload --file <reviewed-raster-file> --content-type image/png --data @scope.json --confirm --json
MCP upload after the same review:
{
"tool": "topolo_upload_action",
"arguments": {
"actionId": "app_topolo_auth.workspaces.avatar.upload",
"fileName": "fixture-pixel.png",
"contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a8ioAAAAASUVORK5CYII=",
"contentType": "image/png",
"input": {
"appId": "app_topolo_web",
"organizationId": "org_avatar_fixture",
"workspaceId": "ws_avatar_fixture"
},
"confirm": true
}
}
Replacing an uploaded raster deletes the previous stored object
Input · payload.json:
{
"appId": "app_topolo_web",
"organizationId": "org_avatar_fixture",
"workspaceId": "ws_avatar_fixture",
"fileName": "fixture-pixel.png",
"contentType": "image/png",
"contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a8ioAAAAASUVORK5CYII="
}
CLI:
topolo actions validate app_topolo_auth.workspaces.avatar.upload --data @payload.json --json
topolo actions plan app_topolo_auth.workspaces.avatar.upload --data @payload.json --confirm --json
topolo actions call app_topolo_auth.workspaces.avatar.upload --data @payload.json --confirm --detail --json
MCP call after topolo_validate_action and topolo_plan_action on this same input:
{
"tool": "topolo_call_action",
"arguments": {
"actionId": "app_topolo_auth.workspaces.avatar.upload",
"input": {
"appId": "app_topolo_web",
"organizationId": "org_avatar_fixture",
"workspaceId": "ws_avatar_fixture",
"fileName": "fixture-pixel.png",
"contentType": "image/png",
"contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a8ioAAAAASUVORK5CYII="
},
"confirm": true,
"detail": true
}
}
Captured local handler response · HTTP 200:
{
"success": true,
"message": "Workspace avatar updated",
"data": {
"workspace": {
"id": "ws_avatar_fixture",
"appId": "app_topolo_web",
"organizationId": "org_avatar_fixture",
"name": "Creative",
"avatar": {
"kind": "image",
"url": "https://auth.topolo.dev/api/avatars/00000000-0000-4000-8000-000000000002"
},
"slug": "creative",
"status": "active",
"isDefault": false,
"ownerUserId": "usr_avatar_owner",
"createdByUserId": null,
"createdAt": 1,
"updatedAt": 1790980947,
"archivedAt": null
}
},
"timestamp": "2026-10-03T00:00:00.000Z"
}
File upload convenience · use the same reviewed bytes and published upload contract:
{
"appId": "app_topolo_web",
"organizationId": "org_avatar_fixture",
"workspaceId": "ws_avatar_fixture"
}
Save this scope as scope.json; the CLI derives fileName/contentBase64 from the reviewed local file. Validate and plan its exact derived payload before confirming:
topolo actions upload app_topolo_auth.workspaces.avatar.upload --file <reviewed-raster-file> --content-type image/png --data @scope.json --confirm --json
MCP upload after the same review:
{
"tool": "topolo_upload_action",
"arguments": {
"actionId": "app_topolo_auth.workspaces.avatar.upload",
"fileName": "fixture-pixel.png",
"contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a8ioAAAAASUVORK5CYII=",
"contentType": "image/png",
"input": {
"appId": "app_topolo_web",
"organizationId": "org_avatar_fixture",
"workspaceId": "ws_avatar_fixture"
},
"confirm": true
}
}
Verify the result
-
Read workspaces.list for the same app/organization and match the exact workspace and returned image URL.
-
Read avatars.content with the discovered UUID and compare decoded bytes/MIME to the reviewed file; verify rendering and provider persistence independently.
Errors and recovery
MIME/signature disagreement is refused before storage
Input:
{
"appId": "app_topolo_web",
"organizationId": "org_avatar_fixture",
"workspaceId": "ws_avatar_fixture",
"fileName": "fixture-pixel.png",
"contentType": "image/jpeg",
"contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a8ioAAAAASUVORK5CYII="
}
Captured local refusal · HTTP 400 · application/json:
{
"success": false,
"error": {
"message": "Image type does not match its content",
"code": "VALIDATION_ERROR",
"details": {
"avatar": true
},
"timestamp": "2026-10-03T00:00:00.000Z"
}
}
Missing storage becomes a workspace handler server error
Input:
{
"appId": "app_topolo_web",
"organizationId": "org_avatar_fixture",
"workspaceId": "ws_avatar_fixture",
"fileName": "fixture-pixel.png",
"contentType": "image/png",
"contentBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+a8ioAAAAASUVORK5CYII="
}
Captured local refusal · HTTP 500 · application/json:
{
"success": false,
"error": {
"message": "Could not update workspace avatar",
"code": "SERVER_ERROR",
"details": null,
"timestamp": "2026-10-03T00:00:00.000Z"
}
}
Published recovery guidance
-
state_conflict(HTTP 409): Read the current resource state, reconcile the intended change, and retry only if still valid. -
AUTH_ERROR(HTTP 401): Verify the pinned identity and cookie/Bearer context. Refresh only an actually expired credential; do not substitute another identity after target denial. -
FORBIDDEN(HTTP 403): Resolve the same app/organization/workspace and its exact management authority. An ordinary seated member or organization admin is not authority over another owner’s workspace; respect refusal. -
NOT_FOUND(HTTP 404): Rediscover the existing exact-app workspace. This action does not provision it. -
VALIDATION_ERROR(HTTP 400): Validate the declared input. For Brand selections discover current published logo/icon choices; for uploads correct encoding, signature, MIME and decoded size. -
SERVER_ERROR(HTTP 500): Read canonical workspace/profile state before retrying: the write may have committed before old-object cleanup. Preserve sanitized evidence and use the authorized maintainer for storage/upstream incidents; no direct storage repair is published here.
Recovery boundaries
Read canonical state after an unknown result. To restore an earlier image, separately review uploading its retained original bytes; this creates a new public URL. For an earlier preset/Brand selection, review workspaces.avatar.set with that saved reference. No rollback restores the previous opaque id or recalls downloaded copies.
Discovery and evidence
This record belongs to Auth. Candidate human/machine URLs become valid only after native publication. Real canonical core/access migrations, identity/workspace SQL, raster validation, Brand response parsing and JSON/binary response construction were exercised. In-memory image storage, identities, seating, Brand workspace discovery and HTTP responses, ids/clock and fault injection are simulated. No live user photo, workspace avatar, Brand publication, OAuth state or Cloudflare object was changed.