When the server has no storage configured, both routes answer
501 uploads_unavailable and GET /v1/session reports uploads_available: false — read that flag before showing an upload button, rather than hitting
the 501 mid-flow.The flow
1
Ask for a signed URL
2
PUT the file straight to the bucket
3
Save the public URL
The organization logo
Identical, with two differences: it needsorganization:update, and the URL is
saved with PATCH /v1/organization.
What is allowed
An allowlist, not a blocklist — deliberately. These files are served back from a
public host, and
text/html among them would be a stored-XSS vector on that host.
Content type and size are part of the signature, so the bucket enforces them.
The check on the API side is there to give you a readable error, not to be the
enforcement.
Replacing an image
Every upload gets a random object key, so replacing an image never overwrites the old object. A URL cached in a CDN or an email cannot start serving somebody else’s picture. The old object is left behind. Pruning is a housekeeping job, not part of the request.Doing it from the browser
The three steps work the same from client code. A presigned URL is short-lived and bound to one object, one content type and one size, so handing it to a browser gives away nothing reusable.A session token in browser code is only as safe as where you keep it. If your
app has a server, the shape worth copying is to sign the first and third calls
there and let the browser do only the
PUT — the bucket upload is the only one
of the three that needs to happen from the device holding the file.