storage.jurisdiction must equal the immutable project region; offline init defaults to us unless —region eu is supplied and preserves existing config. Run ohmyhost init --dry-run --json after adding the capability; the inspection reports a missing runtime client instead of treating configuration alone as a working upload.
Use the private runtime client
ImportcreatePrivateStorageClient from @ohmyhost/customer-runtime/storage and configure it from the hosted environment:
FILES bucket or customer-managed R2 credential to copy into the app.
Use upload, or reserveUpload followed by its signed PUT and completeUpload. A pending completion still needs observation. Repeat upload() after uncertain completion with the same bytes, objectKey and idempotencyKey: the create-only PUT’s 412 completes the existing reservation. Signed upload rejection is retryable only for 5xx or 429; durable multi-object workflows persist transfer IDs before PUT and reconcile completeUpload(transferId). createSignedRead provides a short-lived read capability; confirmed deleteObject completion releases stored quota. Keep signed URLs and keys out of source, logs and project notes.
Files are create-once logical names per immutable data identity. Shared Dev/Prod use one namespace; isolated data uses separate namespaces. Deleting an object or letting a reservation expire never permits reuse in the same identity. A Dev reset assigns a fresh empty namespace, where the same logical name is reusable. Assignment changes keep existing physical locators without copying/moving blobs.
The physical bucket quota is 1 GiB including open reservations; storage_quota_exceeded needs cleanup or a smaller upload. Object keys are 1–512 UTF-8 bytes, slash-separated segments of 1–128 letters/digits/.- starting and ending with a letter/digit. Idempotency keys use 1–128 letters/digits/.:-, and contentType is lowercase type/subtype with no parameters. Bad input is gateway_request_invalid; reused names or conflicting idempotency requests are storage_conflict. Expired uploads need both a new objectKey and idempotencyKey. Signed PUT/GET capabilities last at most five minutes; mint reads per response, not long-lived cache entries. Delete-pending waits until retryAt before the same delete key is retried. A data_change_in_progress response means wait for that operation; storage_binding_data_changed needs redeployment of that environment.
Read or delete several objects
Keep the application manifest of logical keys in its own database; the runtime exposes no bucket-list API.readMany and deleteMany each accept 1–4,000 distinct keys and at most 4 MiB control JSON/framing. Object payloads stream separately:
gateway_response_invalid; a cancelled/incomplete read is never a successful batch. Each found result includes contentType, contentLength and etag plus the stream. Delete idempotency keys must also be distinct within the batch. Ordered outcomes are completed, pending with retryAt, or failed with code/retryable. Persist each outcome; retry only pending/retryable work with its original per-object key. Batch/page selection so the complete invocation fits its ten-subrequest and CPU limits.
A completed deletion verifies that the file’s payload is erased and prior conditional uploads cannot restore it. An accepted request, elapsed upload link or old absence receipt alone does not establish completed payload erasure. Large or interrupted batches may complete only some objects; keep all original keys to resume the rest. deleteMany uses one configured timeout across headers and complete JSON. A timeout may leave durable progress, so observe/retry the original operation rather than assuming that nothing happened. Binary readMany uses an idle timeout for each underlying stream read, rather than an absolute timeout for a whole frame or stream, and requires complete iteration.