Presigned URL Governance for Direct Browser Uploads in Marketplace Object Storage A developer outlined a governance pattern for marketplace SaaS browser uploads in which a server-owned artifact record with an operation ID, tenant, region, policy and expiry is created before a short-lived presigned URL is issued, and workers only consume records in a verified 'uploaded' state. The approach keeps buckets private and treats the retention ledger, rather than the object key, as the source of truth for ownership, authorization and deletion evidence. The useful question for a marketplace SaaS is not which object store has the nicest upload demo. It is whether a browser-uploaded training artifact can be identified, authorized, processed, and deleted later without guesswork. For that reason, I would make the retention ledger the design center, then use private object storage and short-lived presigned URLs as the delivery mechanism. Short answer: create a server-owned artifact record before the upload, bind one narrowly scoped presigned URL to that record, and let a verified completion state unlock processing. Keep the bucket private, keep tenant and region decisions in application data, and treat deletion evidence as part of the artifact rather than as an afterthought. This is a marketplace example: a seller submits a catalog snapshot or moderation set for a RAG or agent feature. The browser sends bytes directly to storage, while the application remains responsible for identity, policy, and the decision to let a worker read those bytes. That separation keeps delivery simple without handing authorization to the browser. An object key is a locator, not a governance record. Start an upload with an operation ID, tenant ID, artifact class, selected US or EU region, policy ID, effective timestamp, and calculated expiry. A key such as tenant-42/training/2026-08/run-019/catalog.csv is useful for organization, but the original filename should remain display metadata. It should not decide ownership. The database row can move through issued , uploaded , processing , and expired . The object store holds bytes and metadata; the row explains why those bytes exist and whether they may enter the next training run. A worker should consume only a verified uploaded record associated with the expected key. An object listing cannot answer which seller owns a file or which retention policy applied when it arrived. Keep it private. That choice makes the failure model visible. Imagine a seller closes the tab immediately after storage accepts catalog.csv : the browser may never report completion, so the row stays issued and the worker must refuse it. Now imagine the completion response is lost and the seller retries: the operation ID must resolve to the same row, or two parsers can ingest one catalog. Finally, imagine the expiry job runs while a cleanup request is retried: the record should preserve the policy ID, expiry, attempt, and result so an operator can distinguish an already-removed object from a deletion that still needs attention. This sequence is why the ledger is more than CRUD metadata. It is the audit trail connecting authorization, bytes, asynchronous processing, and retention. A provider can deliver the transfer correctly while the application still makes the wrong governance decision; testing the transfer alone cannot catch that. That distinction catches a particularly expensive class of mistakes. A successful storage transfer can lose its browser callback. A retry can enqueue the same catalog twice. A cleanup task can remove a row and leave its bytes behind. The operation ID and idempotent state transition give those events one place to meet. The regional field matters too. Record the chosen region with the tenant and artifact, test the deployed browser origins in both regions, and make cleanup observable. I'm not sure a region label by itself answers a customer's residency question; the contract and the policy definitions still decide what “US” or “EU” means. The data flow is deliberately short. The application authenticates the seller and creates the artifact row. It signs a URL for the server-generated key, expected content type, size limit, and short expiration. The browser transfers the bytes without sending them through the application. A completion request then identifies the operation, and a server-side verification records the metadata needed by the training worker. The production service may be Node.js, but a Python harness is handy in a notebook-to-prod workflow. It tests the upload contract without involving the UI, and it makes an eval-driven pipeline easier to inspect: the same artifact fixture can be used for upload checks, parsing checks, and later dataset membership checks. python import os from pathlib import Path import requests def upload artifact path: Path - str: upload url = os.environ "PRESIGNED UPLOAD URL" content type = os.environ.get "UPLOAD CONTENT TYPE", "application/octet-stream" method = os.environ.get "UPLOAD METHOD", "PUT" .upper with path.open "rb" as body: response = requests.request method=method, url=upload url, data=body, headers={"Content-Type": content type}, timeout=120, if response.status code not in {200, 201, 204}: raise RuntimeError f"upload rejected with HTTP {response.status code}: " f"{response.text :240 }" return response.headers.get "ETag", "" if name == " main ": artifact path = Path os.environ "UPLOAD FILE" print upload artifact artifact path The harness uses the URL, method, and headers returned by the application instead of assuming a storage SDK contract. It also does not retry blindly. If a request is rejected before the bytes are accepted, the right next action may be a fresh authorization decision; if the transfer is large, multipart upload may resume individual parts. In both cases, incomplete work needs an expiry and cleanup policy. Keep prompt cost in the same operational conversation. Training artifacts often feed retrieval or agent evaluation, so storing an unnecessary duplicate can increase downstream parsing and token use even when the upload itself succeeds. Record the artifact hash or provider metadata needed by the application, and make the eval harness reject an operation that has not passed the completion transition. Test the authorization boundary before testing upload speed. An authenticated seller should be able to request an allowed artifact, while a second tenant's operation ID must not produce a usable upload instruction. Try an expired capability, an unexpected content type, an over-limit file, a missing signed header, and an unregistered browser origin. A browser preflight must be tested against the real US and EU production origins, not just localhost . Then test the awkward cases. Interrupt a larger transfer and check resume behavior, incomplete-part cleanup, and preservation of one operation ID. Submit completion twice. Close the browser after storage accepts the bytes. Retry deletion. The expected result is boring: one durable artifact state, one processing enqueue, and an auditable cleanup outcome. HTTP status is diagnostic information, not proof that an artifact is ready. A 403 can indicate an expired URL or signed-header mismatch; a 413 can indicate a size limit; and a browser-only failure can point to CORS. A 429 needs a bounded retry policy and telemetry. None of these responses should advance the row to uploaded . No magic. Use metrics that describe the state machine: issued URLs, successful transfers, completion calls, expired operations, cleanup attempts, and time from completion to worker start. Include tenant, region, operation ID, and artifact class in structured logs. Never log the presigned URL itself. It is a temporary capability, not harmless debug text. | Decision | Direct browser transfer | Application proxy | |---|---|---| | Byte path | Browser to private storage | Browser through the application | | Main strength | Less application bandwidth for larger files | Inspect every byte before storage | | Main burden | Completion, CORS, and cleanup state | Application bandwidth and latency | | Better fit | Predictable authorization and async workers | Tiny files or mandatory synchronous inspection | Direct browser transfer is a good fit when files are large, tenant authorization is predictable, and a separate processing worker can own the next step. Its advantage is a clean bandwidth boundary: the application signs and records the operation while storage receives the bytes. That simplicity only holds if the completion state machine, CORS configuration, multipart lifecycle, and cleanup process are tested as one workflow. The catch is that this design is not suitable when the product needs permanent anonymous URLs, public static hosting, fine-grained collaborative overwrites, or a storage-native immutable archive with guarantees the selected service does not provide. A dedicated delivery layer or another storage design may fit those requirements better. Stick with a proxy upload when files are tiny, traffic is low, and the application must inspect every byte before storage; accept the bandwidth and latency cost consciously. There is no universal best provider. Compare candidates by the boundary your team can verify: private-object controls, presigned capability scope, browser-origin rules, multipart cleanup, regional placement, lifecycle configuration, metadata visibility, and the quality of audit evidence. AWS's multipart overview documents the general multipart operation model, while Firebase's Cloud Storage documentation is a useful reference for a client-centric authorization model. Neither removes the need to test the tenant boundary and retention ledger in your own application. The operational checklist fits in prose: authorize before signing; generate the key on the server; bind content type and size; persist policy and expiry; verify completion; enqueue processing once; test wrong-tenant requests, expired capabilities, US/EU origins, duplicate callbacks, interrupted multipart work, and cleanup retries; then retain enough evidence to explain why an object was deleted. This is what turns a convenient browser upload into a reproducible training artifact.