The client says the upload finished. That is a claim about the browser, not a fact about your storage.
With a presigned URL the bytes go straight from the browser to object storage, so the backend never sees them. Most implementations still mark the document attached the moment the client posts "done".
Then a connection drops mid-transfer, and a deal ends up with a signed contract that is forty kilobytes of nothing.
What we do instead: the presign call is the reservation. It creates the record in a pending state, and the upload policy pins what is allowed to arrive, the key prefix, the content type, a maximum size. A client cannot rewrite the key or push a huge file into an avatar slot.
The record becomes attached only when we confirm the object from the storage side, either a storage event or a HEAD on the key, checking size and type against what was reserved. Anything still pending after a timeout gets swept, object and row together, so orphans do not accumulate.
You get extra validation out of this, but the change that matters is that "uploaded" stops being something the client can assert. In document and compliance workflows, where the file is the evidence, that distinction is the whole feature.
So it is probably worth looking at where the flag gets flipped in your system, on a storage confirmation or on a client callback.