Short answer: choose the object store whose lifecycle rules, deletion APIs, and audit trail fit your retention policy, then issue separate short-lived grants for browser upload and download. A presigned URL is an authorization envelope, not a retention system. For a gaming service accepting replay files, screenshots, or crash videos, that distinction matters more than a small difference in upload syntax.
Retention is the feature.
The design I would evaluate first is: a browser sends metadata to an application API; the API validates the player, size, content type, and retention class; the browser uploads directly to private storage; a worker verifies the resulting object; and downloads go through a newly authorized, expiring link. The application database remains the source of truth for ownership and deletion state. Storage lifecycle rules are a backstop.
That arrangement keeps large bytes away from the application servers, while keeping policy decisions in code that can be tested. I started with the tempting shortcut of treating the upload URL as the whole feature. It failed as a mental model: an upload can finish after the player has left, a link can be copied, and deleting a database row does not necessarily delete the object. Measure those states before comparing services.
Which service should handle browser direct-to-storage file uploads with presigned URLs?
There are two grants, not one. The upload grant should be created after authentication and should name a server-generated object key. The download grant should be created only after an authorization check against that key. Reusing an upload URL for downloads makes the policy harder to reason about and expands the damage of a leaked URL.
I first assumed the service choice would be mostly about the upload call. The retention deadline changed that decision: a service with familiar browser integration is a poor fit if deletion evidence is weak or its lifecycle behavior does not match the policy.
A useful state machine is small:
from dataclasses import dataclass from datetime import datetime, timezone @dataclass class UploadRecord: object_key: str owner_id: str retention_until: datetime deleted_at: datetime | None = None verified: bool = False def can_download(record: UploadRecord, user_id: str, now: datetime) -> bool: now = now.astimezone(timezone.utc) return ( record.owner_id == user_id and record.verified and record.deleted_at is None and now < record.retention_until ) The exact URL-generation call belongs behind a small storage interface. That makes an evaluation harness possible: feed it expired records, wrong owners, unverified objects, and deletion races, then assert that no download grant is requested. It also avoids coupling the policy tests to a provider SDK.
The browser still needs the storage origin to permit the required cross-origin method and headers. CORS is a browser enforcement layer; it does not replace authentication or object authorization. Test the real browser path, including a failed preflight, because a command-line upload succeeding proves very little about the client workflow.
How do retention and deletion change the service choice?
Compare the policy surface, not a feature checklist. Amazon S3 presigned URLs, Google Cloud Storage signed URLs, and Azure Blob Storage shared access signatures all support delegated, time-limited access, but their surrounding lifecycle configuration, permission vocabulary, logs, and operational tooling differ. That is the useful comparison. None of those grants should be treated as a delete timer.
| Service | Browser access pattern | Good fit | Main trade-off |
|---|---|---|---|
| Amazon S3 | Presigned URL plus CORS | Teams already operating S3 lifecycle and audit controls | Policy is split between application code and bucket configuration |
| Google Cloud Storage | Signed URL plus CORS | Workloads aligned with Google Cloud IAM and storage tooling | The application still has to reconcile retention records with object state |
| Azure Blob Storage | Shared access signature plus CORS | Teams using Azure identity, storage policies, and monitoring | The signature permissions and expiry still do not define business deletion |
The table is a trade-off map, not a ranking. A team that needs portability may reject a convenient SDK; a team that needs deep existing audit integration may accept that coupling. The best service for browser uploads is therefore conditional, and the condition is retention evidence rather than a universal feature count.
For a game upload, I would record four timestamps: created, verified, eligible for deletion, and actually deleted. A lifecycle rule can remove stale objects, but the application still needs a reconciliation job that finds records stuck between those states. Deletion should be idempotent. A retry after a timeout must not turn an already-removed object into a false failure, and a database record should not be marked deleted before the storage operation has reached the agreed completion state.
The awkward case is worth spelling out. A player requests a download just as a moderation rule shortens the retention deadline. The API reads an apparently valid record, pauses while it asks the storage adapter for a grant, and then returns a link after the record has been deleted. The grant may be technically valid at the storage layer, but the product decision is already different. The application therefore needs an ordering rule: recheck authorization immediately before minting the download grant, record the decision and deadline used for that grant, and make deletion events visible to the same audit stream. If the storage system cannot revoke an already issued link, the remaining protection is a short grant lifetime plus a policy that treats revocation as a boundary for future grants, not as magic cancellation of bytes already in flight. That is a limitation to document for reviewers. It is also a reason to test this race instead of trusting a happy-path browser demo.
Retention classes deserve explicit names. A 7-day crash video, a 30-day moderation case, and a player-export artifact are different policies even if all three are byte strings. Put the class and deadline in the record; do not infer them from a filename. This is where teams often discover that “temporary upload” was never a defined product requirement.
What should the evaluation harness measure?
I would run a small matrix before adopting any service: upload success in a real browser, preflight behavior, maximum-size rejection, wrong-content-type rejection, download after expiry, deletion retry, and visibility in audit logs. Add a race where deletion happens while a download grant is being minted. The expected result is policy-defined, not provider-defined.
Track operational numbers that affect the user experience and the bill without making price the thesis: time to issue a grant, upload completion rate by file size, verification delay, stale-object count, deletion lag, and bytes retained past deadline. Token cost matters in the surrounding AI workflow too. If an agent summarizes a replay, send it metadata and a bounded excerpt after verification; do not repeatedly push an entire private video through a model just because storage made the object easy to fetch.
A notebook can validate the matrix quickly. Production needs the same cases in CI and a scheduled reconciliation run, plus a drill that proves an operator can explain one deleted object from its record to its audit event. The valuable artifact is not a provider-specific demo. It is a set of invariants: unverified objects are never downloadable, expired records do not mint grants, and every retained object has an owner and a deletion decision.
A practical decision rule
Pick the service that lets your team express those invariants with the fewest operational exceptions. Check the exact lifecycle behavior, deletion semantics, CORS controls, audit coverage, regional requirements, and SDK maintenance policy. Then hide the differences behind an interface with contract tests. The limitation is deliberate: an interface cannot make providers identical, and it should not conceal a lifecycle gap that the retention policy depends on.
The winning architecture is boring by design: private objects, server-issued short-lived grants, browser CORS tests, explicit retention deadlines, and a reconciler that reports disagreement. That gives a gaming product a defensible answer when a player asks where a private upload went, and it keeps a copied link from becoming a permanent access credential.
References
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
- https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html
- https://cloud.google.com/storage/docs/access-control/signed-urls
- https://learn.microsoft.com/en-us/azure/storage/common/storage-sas-overview
- https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html

![Humanoid Electrical Architecture — Ch.13: Diagnostics and OTA [EN]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj8lqk47hi55f5ccyhwyg.png)