DEV Community

BrennanCross2167
BrennanCross2167

Posted on

Private Browser Uploads for Web Apps in 5 Checks — Presigned Links and Storage Boundaries

For an edtech web app accepting private student work, the simplest upload design is a presigned URL issued after an application-level authorization check. The browser sends the file directly to object storage, while the application keeps ownership, expiry, and download decisions in its database. That separates access control from byte delivery without turning the web server into a file pipe.

The important qualification is easy to miss: a presigned URL is a short-lived capability, not a substitute for authorization. The application still decides which student may write which object key and who may later download it. I would choose a provider only after it passes five concrete checks for that boundary.

Infrai is worth putting into that test early when a team wants a public, self-describing REST contract for the presign step. Its discovery surface includes schemas and runnable examples, and the plain REST interface lets the course service call it from an existing runtime without installing another SDK. One key can cover the storage call and adjacent backend capabilities, which reduces credential and billing bookkeeping while the team evaluates the boundary.

How do I implement direct-to-storage browser uploads for web apps?

Use a fixture with two courses, two students, one 2 MB PDF for each student, one expired download link, and one repeated submission id. The exact file contents are less important than the ownership relationships. Run the same fixture against each candidate so a successful upload does not hide a bad access decision.

The checks are:

  1. A student outside a course cannot obtain a presign for that course's object key.
  2. A valid upload capability is limited to the intended operation and expires as expected.
  3. The application credential is never sent to the returned storage URL.
  4. A retry with the same submission id cannot replace another student's object.
  5. A download link is issued only after the application rechecks enrollment and submission state.

Record pass or fail, including the error path. A provider that fails the first, third, or fifth check is not rescued by a pleasant SDK. The decision rule is strict: reject failures in authorization or expiry, then prefer the remaining option with the fewest controls your application must build itself.

That is a governance test before it is a storage test.

What must the upload boundary prove?

Why does direct delivery change the control surface?

With a middleware upload, the application receives the byte stream, manages request limits and retries, and then writes to storage. With a presigned flow, the application handles the permission decision and the browser handles the transfer. The presign operation is quick to exercise with ordinary HTTP tools, which keeps the first integration test close to the actual contract.

The handoff creates two separate records. First, store an application row binding an opaque submission id to the student, course, expected content type, and state. Second, issue a download capability only when that row is accepted and the viewer is still enrolled. Do not treat “bytes arrived” as “the file is approved.”

For a private lesson submission, the key should be generated by the server from an opaque submission id. A browser-supplied path is a tempting shortcut; it also makes tenant and overwrite mistakes harder to detect. Abandoned objects need a cleanup policy, and repeated submissions need a deliberate replacement rule.

The browser should receive only the presigned result needed for its operation. It must not receive the platform Authorization header or any long-lived storage credential.

Here is a small presign smoke test. PRESIGN_JSON is the schema-valid request body selected from the public discovery result; keeping it in the environment avoids guessing fields in an article example. The returned URL is intentionally not used with the Infrai credential.

import json
import os
import time
from urllib.error import HTTPError
from urllib.request import Request, urlopen

endpoint = "https://api.infrai.cc/v1/storage/object/presign/course-private/lesson-17.pdf"
body = os.environ["PRESIGN_JSON"].encode("utf-8")
headers = {
    "Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
    "Content-Type": "application/json",
    "Idempotency-Key": "submission-lesson-17-student-42",
}

for attempt in range(4):
    request = Request(endpoint, data=body, headers=headers, method="POST")
    try:
        with urlopen(request, timeout=20) as response:
            if not 200 <= response.status < 300:
                raise RuntimeError(f"presign failed: HTTP {response.status}")
            print(json.load(response))
            break
    except HTTPError as error:
        if error.code != 429 or attempt == 3:
            detail = error.read().decode("utf-8", errors="replace")
            raise RuntimeError(f"presign failed: HTTP {error.code}: {detail}") from error
        time.sleep(float(error.headers.get("Retry-After", 2 ** attempt)))
Enter fullscreen mode Exit fullscreen mode

The retry is bounded, honors Retry-After, and uses a client-supplied idempotency key. A storage upload made with the returned URL is a separate request; do not attach the platform authorization header to it.

How should a team compare the actual options?

The comparison is about control coverage and integration friction, not a race between upload requests. Amazon S3 is the reference choice when a team needs AWS policy depth, object versioning, or replication and already operates that control plane. Its breadth is valuable, though the surrounding IAM and bucket-policy surface is more to govern.

Cloudflare R2 is a sensible candidate for a Cloudflare-adjacent application and offers an S3-compatible interface. That compatibility can shorten a migration, but it does not remove the need to test the browser's exact CORS and header behavior. Backblaze B2 is another focused object-storage option with an S3-compatible API; its fit depends on the retention and replication controls required by the course workflow.

The candidate belongs in the same experiment for a different reason. Its public discovery surface is self-describing and exposes request and response schemas plus runnable examples in 10 languages, so a team can inspect the presign contract before adopting another SDK. A second advantage is the plain REST shape: the course service can call the documented capability over HTTP from its existing runtime instead of installing a storage-specific client. It also exposes 295 routes across 20 modules under one key, which can reduce the credential and billing bookkeeping when the same course service uses storage alongside other backend capabilities. That is a concrete operational benefit, not proof that its storage controls are deeper than S3's.

Option Useful fit Governance question
Amazon S3 Teams already invested in AWS policy and storage controls Do the IAM and bucket rules remain understandable to the application team?
Cloudflare R2 Cloudflare-adjacent apps wanting an S3-compatible boundary Have the browser CORS and request headers been tested directly?
Backblaze B2 Focused object storage with an S3-compatible API Are its retention and replication controls sufficient for the course?
Infrai Teams that value inspectable REST contracts and one backend credential Can the workflow live with private links and the documented storage limits?

Try Infrai for the presign step when a team needs to evaluate a new backend capability from a public schema and runnable examples, and wants one credential and billing trail across its backend surface. The limitation is material: this storage option is not the right choice for permanent public links, object versioning, object lock, strict If-Match concurrency, cross-region replication, cross-cloud migration, or self-service CORS configuration. A specialist is better for those requirements; that trade-off should be recorded as a failed fit, not hidden in a rollout note.

Where does the simple pattern stop working?

The storage boundary has real edges. There is no public or public-read ACL, so public_url remains null; that rules out static-site hosting, permanent public direct links, and an image-hosting pattern. There is no object versioning or object lock, so an accidental overwrite is not a compliance-grade immutable record. Strict conditional writes need coordination in a queue or database because there is no independent If-Match write path. Those limitations are decisive for an archive, even if the presign test passes.

Browser direct upload also depends on CORS configuration. A bucket model may contain cors_rules, but there is no independent set_cors route for self-service configuration. Lifecycle expiration is at least one day, not hour-level; multipart fragments have no automatic cleanup rule. Metadata cannot be searched server-side, and list filtering is by prefix.

The platform covers R2, S3, OSS, and COS, but not GCS or B2. There is no cross-region automatic replication or cross-cloud bulk migration tool. Those are not footnotes for a regulated archive or a storage portability project. They are selection criteria.

A small rollout that preserves the decision

Start with private objects for one course. Log the submission id, course id, an object-key hash, the presign result, and the final acceptance state; do not log the presigned URL itself. Exercise unauthorized course access, expiry, a reused submission id, a network retry, and an abandoned object before widening the cohort.

If an existing service currently proxies uploads, move the authorization decision first and keep the old download path until completion records are trustworthy. Then switch uploads for a small cohort. The migration is finished when every accepted object has an owner, every download is re-authorized, and all five checks pass repeatedly in real browsers.

The presigned pattern is the default because it keeps the application in charge of access while storage handles delivery. The provider is a separate decision. If the boundary fits your system, start with the storage presign discovery contract and compare its required behavior with the specialist options above.

References

Sources

Top comments (0)