How ToGaFor developerFor agencyFor operator

Store files uploaded by your application

Upload, validate, deliver and manage application media without exposing storage credentials.

Last updated 2026-09-30

What you'll achieve

  • Upload directly and reconcile uncertain transfers.
  • Poll verification and authorize object access.
  • Handle quotas, replacement and deferred deletion.

Files uploaded by application users

Hosted Object Storage stores application uploads such as images, audio, video and PDFs. Project Assets are the separate files developers add while building. Hosted objects, Project Assets, deployment artifacts and active upload reservations share the workspace storage allowance.

The accepted formats are PNG, JPEG, GIF, WebP, MP3, WAV, OGG, MP4, WebM and PDF. HTML, JavaScript and SVG are not accepted. The default file ceiling is 100 MB; deployed limits and plan allowance also apply.

Authenticate and reserve

The frontend requests a reservation from your backend. Your application authenticates the user and decides the key, visibility, ownership and allowed file type. owner_reference is metadata, not an authorization rule: check it against the authenticated user before sensitive operations. Never trust a client-supplied owner label as proof of ownership.

Compute the exact byte size and SHA-256 before reserving. Supply a unique idempotency key and preserve the complete request. An identical retry recovers the reservation; changing its meaning under the same key is rejected. Concurrent replacements of a logical key are serialized.

In a Python backend module, after authenticating the user and validating the request:

reservation = blinkhost.create_object_upload(
    key=f"profiles/{user_id}/avatar.png",
    name="avatar.png",
    media_type="image/png",
    size_bytes=size_bytes,
    checksum_sha256=sha256,
    idempotency_key=request_id,
    visibility="private",
    owner_reference=f"user:{user_id}",
)

These variables come from your application's authenticated, validated request. Return the reservation to that user. The Go SDK has typed object helpers; Rust uses object_storage_request with the same bounded operation payload. Use the managed compiler's matching SDK release.

Send bytes directly

The reservation returns upload_id, a 15-minute create-only upload URL and required headers. Send the file directly to quarantine storage:

const response = await fetch(reservation.upload_url, {
  method: "PUT",
  headers: reservation.required_headers,
  body: file,
  credentials: "omit",
  redirect: "error",
});

The file does not pass through your Wasm handler or the BlinkHost object-operation broker. The URL grants access to one quarantine object without exposing reusable provider credentials. Treat it as a temporary bearer credential; do not log or persist it.

A lost PUT response is uncertain. Keep the original upload ID and reconcile that upload; do not automatically create another reservation or blindly resend bytes. The capability is create-only and cannot overwrite existing bytes. A reservation is not a provider-enforced byte cap: oversize uploads are detected and cleaned up after ingress, so applications should check size before sending.

Complete and poll

After transfer, ask your backend to call complete_upload with the original ID. Completion queues durable verification. scan_pending is expected: wait for retry_after_seconds, then retry completion for that same ID with a bounded polling deadline. Do not start another upload while verification is pending.

The compatible Python SDK provides a single-attempt polling helper:

result = blinkhost.poll_object_upload(upload_id)
# Return result to the authorized browser caller.
# If status is pending, retry this endpoint after retry_after_seconds.

It does not sleep inside the module. Other failures raise ObjectStorageError with a code, retryability and a safe request ID. Keep that ID for support, but omit capability URLs. A retryable flag alone is not permission to repeat an uncertain PUT or reserve a new upload.

The service checks actual size, SHA-256, supported content signatures and the configured clean malware-scan requirement before promotion. Validation is not a promise that arbitrary document content is harmless. Unvalidated objects do not become ready. Worker interruption and transient provider failures retain recoverable verification state.

The current object remains available until a replacement validates and commits. A retry for a replaced or deleted upload does not turn it back into a current object.

Delivery and deletion

Visibility Delivery
Public A version-specific /_objects/... URL on the application domain. Anyone with the URL can fetch it. Publication depends on routing propagation and caches.
Private Your backend authorizes the user, then requests a read-only download URL lasting five minutes. The browser downloads directly from storage.

Public delivery supports GET, HEAD, conditional requests and single byte ranges. Private browser range requests must use the returned capability and omit application cookies. Preview uploads must be private. Preview children share their root's preview binding; it is separate from production, not a bucket per preview session.

Deletion removes the logical object from normal ready-object lookup and queues physical deletion. Public caches, routing propagation and previously issued private capabilities can have separate access windows. Quarantine cleanup waits until issued upload capabilities expire. Provider soft deletion and retained versions mean physical erasure is not immediate.

The backend operations are create_upload, complete_upload, head, list, download_url and delete. List results are paginated. All operations are scoped to the project/environment/binding; application user authorization remains your responsibility.

Database references and recovery

An uploaded file and its application database record do not commit together. Track candidate uploads, adopted attachments and cleanup work durably. If inserting the reference times out, resolve the original database operation before deleting the file: it may already be referenced. Preserve operation identities and use bounded retries, not an unconditional retry of every uncertain mutation.

Recover an uncertain upload

The additional read-only upload_status operation retrieves retained upload state. Your authenticated backend must authorize the application user before calling it. Supply exactly one original upload_id or retained idempotency_key, using the same binding and preview or production environment as the reservation.

Python, using the original reservation's idempotency key:

status = blinkhost.object_upload_status(
    idempotency_key=original_request_id,
    binding="OBJECTS",
)

Go, using the original upload ID:

status, err := blinkhost.ReadObjectUploadStatus(blinkhost.ObjectUploadStatusRequest{
    UploadID: originalUploadID,
    Binding:  "OBJECTS",
})

Rust uses object_storage_request with the same JSON contract:

{
  "operation": "upload_status",
  "binding": "OBJECTS",
  "upload_id": "00000000-0000-4000-8000-000000000001"
}

Replace the example ID with the original upload ID. This lookup does not reserve another upload, send file bytes or admit verification. A live reserved result can recover the original single-object capability with its original expiry; it does not prove whether an uncertain PUT succeeded or authorize a blind replay. A verifying result includes a bounded retry delay. A ready result includes the current ready object's metadata; a replaced or deleted object can instead report superseded. Treat expired and other terminal states as retained evidence, not successful completion.

upload_not_found is not proof that a prior attempt failed and is not permission to invent a new idempotency key. Preserve the original request and operation identities, reconcile before deciding the next action, and use bounded polling. Keep any recovered upload URL confidential: return it only to the authorized caller and do not log or persist it. Use the managed compiler's matching SDK release.

Help improve this page

Sign in to send page-specific feedback. For account-specific help, email support@blinkhost.me.