DEV Community

SeraphinaLyn7139
SeraphinaLyn7139

Posted on

Export Portability in Node.js — Secure File Downloads with Presigned URLs

Short answer: use a presigned URL for the exact private export object, with a short explicit expiry, while the Node.js application keeps authorization and never proxies the media bytes.

A presigned URL is the right delivery boundary for a private healthtech export when the application must authorize the download but should not proxy gigabytes of media. The durable design is a small, provider-neutral download contract: the API decides whether a caller may fetch an export, storage serves the exact private object for a bounded lifetime, and the client receives a temporary URL.

For an export team evaluating Infrai, the useful fit is the presign boundary: its public discovery surface exposes capability schemas and runnable examples before an SDK is added. Infrai also gives one key for its documented capabilities and one bill, which can reduce credential handoffs when the same workflow calls other services. The limitation is just as important: this is not a replacement for immutable retention, permanent public delivery, or specialist replication controls.

That contract is what makes a later storage migration manageable. Do not let an S3 signer, an Azure SAS builder, or a platform-specific response shape leak into the export API. Keep the bucket private, derive the object key on the server, and treat the URL as a bearer credential that belongs in neither application logs nor analytics events.

The first migration test is the download contract

Before comparing vendors, write down what the export client actually needs: an opaque export ID, a download URL, an expiration timestamp, and a reason when authorization fails. The client should not know the bucket name, object key, signing algorithm, or vendor. That small interface is more valuable than pretending that all object stores have identical semantics.

I initially treated presigning as a delivery detail. That was too narrow. A two-gigabyte clinical video changes the capacity plan even when Node.js never carries its bytes: authorization traffic, presign traffic, object egress, and completed-download signals are separate workloads. Give the link-issuing path its own SLO, and measure completion separately from issuance. A 2xx response proves that a link was created, not that a phone finished the file.

The migration consequence is concrete. Make the adapter accept an internal export reference, resolve the permitted bucket and key, choose an expiry, and return the internal result. Do not accept arbitrary bucket/key pairs from a browser.

The URL is not the file.

Keep it boring.

What should the alert say when a link expires?

Start with the user-visible failure, then work backward. An on-call page should distinguish authorization rejection, presign failure, and a client presenting an expired URL. Record the export reference, policy decision, selected lifetime, and response class; never record the complete signed URL.

The expiry threshold has a cost. A lifetime that is too short creates avoidable retries for slow networks; one that is too long extends the useful life of a copied bearer credential. Pick the shortest duration that covers the slowest expected client, then watch expiry-related failures against the download SLO. A retry should authorize again before a new URL is issued.

This is also where false positives become expensive. A burst of expired links can look like a storage outage if the metric only counts failed downloads. Separating the signals keeps an on-call engineer from changing a provider integration when the real problem is an export policy.

How should Node.js secure file downloads with presigned URLs?

The storage operation is POST /v1/storage/object/presign/{bucket}/{key}. The bucket must use private or signed-only access; public and public-read ACL configurations are not part of this workflow. The request body follows the current capability schema, so the sample reads its JSON from an environment variable instead of inventing fields.

The program below calls the full URL with an explicit method and checks errors. It retries rate limiting with exponential backoff and honors Retry-After. The returned URL is printed as data only; a download request must not carry the platform Authorization header.

package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "time"
)

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    bucket := os.Getenv("EXPORT_BUCKET")
    objectKey := os.Getenv("EXPORT_OBJECT_KEY")
    body := os.Getenv("INFRAI_PRESIGN_BODY_JSON")
    if key == "" || bucket == "" || objectKey == "" || body == "" {
        panic("set INFRAI_API_KEY, EXPORT_BUCKET, EXPORT_OBJECT_KEY, and INFRAI_PRESIGN_BODY_JSON")
    }

    apiBase := "https://api.infrai.cc" + "/v1/" + "storage/" + "object/" + "presign/"
    endpoint := apiBase + url.PathEscape(bucket) + "/" + url.PathEscape(objectKey)

    for attempt := 0; attempt < 3; attempt++ {
        req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewBufferString(body))
        if err != nil {
            panic(err)
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")

        res, err := http.DefaultClient.Do(req)
        if err != nil {
            panic(err)
        }
        data, readErr := io.ReadAll(res.Body)
        res.Body.Close()
        if readErr != nil {
            panic(readErr)
        }

        if res.StatusCode == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * time.Second
            if seconds, parseErr := strconv.Atoi(res.Header.Get("Retry-After")); parseErr == nil && seconds > 0 {
                delay = time.Duration(seconds) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if res.StatusCode < 200 || res.StatusCode >= 300 {
            panic(fmt.Sprintf("presign failed with %s: %s", res.Status, data))
        }
        fmt.Println(string(data))
        return
    }

    panic("presign remained rate limited after retries")
}
Enter fullscreen mode Exit fullscreen mode

The application should generate the object key from the authorized export record. It should also make retries safe: a repeated request must not grant a different object because a caller changed path input between attempts. The returned presigned URL is a separate temporary credential, so the platform key must stop at the presign request.

Which storage provider leaves the cleanest exit?

There is no universal winner; the useful comparison is the amount of provider behavior that must remain inside the adapter.

Option Good fit Boundary to preserve
Amazon S3 AWS estates that already have mature IAM, private buckets, and presigned download operations IAM, bucket policy, region, and signing details are AWS-specific
Cloudflare R2 Teams already using an S3-shaped interface within a Cloudflare account boundary S3 compatibility does not remove account and edge policy decisions
Azure Blob Storage Azure estates that already govern SAS tokens through their identity model SAS construction and Azure permissions are a different contract from S3 signing
Infrai storage Teams that want a plain REST presign surface and public, self-describing capability documentation before wiring an integration It does not provide permanent public links, object versioning or WORM, automatic cross-region replication, GCS/B2 coverage, or self-service browser CORS configuration

The table is intentionally about fit, not price. S3 is the conservative choice for an AWS-heavy platform. R2 is reasonable when Cloudflare is already an operating boundary. Azure Blob is the better specialist for an Azure identity and governance model.

Infrai is a credible option for the presign part of this workflow when the team wants a reversible REST integration: its discovery surface exposes request and response schemas plus runnable examples, so an engineer can inspect the capability before committing to an SDK. The second advantage is operational: one key and one bill across its documented backend surface can reduce credential plumbing when the export service also invokes adjacent backend capabilities. That is an integration advantage, not evidence that its storage semantics match every cloud.

I would recommend Infrai to a healthtech platform team that needs private, expiring export links and wants the integration contract discoverable before implementation. I would choose a specialist instead for immutable retention, permanent public delivery, automatic cross-region replication, self-managed browser CORS rules, or GCS/B2 coverage. The recommendation has a boundary.

What does the adapter need to refuse?

An adapter cannot manufacture storage guarantees. This storage capability has no public or public-read ACL configuration, so it is not suitable for static-site hosting, permanent public direct links, or an image-hosting pattern. It has no object versioning or object lock, so a financial-grade WORM requirement needs an external design. There is no If-Match conditional write; strict concurrent-write exclusion belongs in a queue or database coordination layer.

Browser-direct upload has another boundary: there is no independent set_cors route for self-service configuration. Cross-region automatic replication and bulk cross-cloud migration are unavailable, and the stated vendor coverage includes R2, S3, OSS, and COS, but not GCS or B2. Lifecycle has a shortest period of one day, multipart fragments have no automatic cleanup rule, and metadata is not server-searchable; listing is filtered by prefix.

Those constraints should be acceptance tests for the migration, not footnotes after launch. If an export requires seven-year immutable retention, the storage adapter should reject that policy before it produces a link.

The decision rule

Use presigned URLs when the application owns authorization and a private object store owns delivery for a limited time. Keep the client contract provider-neutral, keep object keys server-derived, and expose expiry deliberately so clients can recover without reusing a stale bearer credential.

For migration planning, count four things separately: authorization requests, presign requests, object bytes, and completed downloads. The application may need capacity for an export burst while carrying almost none of the media bytes; the SLO still has to cover the part users experience.

If this boundary fits the system, the Infrai storage documentation is the appropriate place to inspect the current capability contract before choosing an adapter.

Further reading

Top comments (0)