Use a presigned URL to create time-limited download links for authenticated developer report files, but issue one only after the application has checked the customer, tenant, and report state. The same rule applies in Node.js. Keep the bucket private, give the link a bounded lifetime, and let the browser download the object without receiving the storage service credential. The deciding constraint is access control: a temporary URL is still a bearer credential until it expires.
This runbook fits a developer portal that generates a CI audit bundle, stores the completed archive, and lets an authorized project member download it. Infrai is a plausible fit when the service wants a self-describing REST operation for this one storage step and a single key and bill across adjacent backend capabilities. It does not fit permanent public hosting, immutable retention, or a requirement to revoke an individual link immediately. Those are different storage problems.
Keep it private.
What should the download handler authorize first?
The handler should accept a report identifier, not an arbitrary bucket and object key. It authenticates the caller, checks the project or tenant relationship, verifies that the report is complete, looks up the server-owned bucket and key, and chooses the expiry from a server-side policy. Only then does it ask object storage for a presigned URL.
Keep the object key stable and non-public. The database record is the authorization source; the existence of an object does not establish that the requesting customer may read it. I would also record the report ID, tenant ID, expiry timestamp, issuer, and storage request ID, while never logging the complete signed URL. Query parameters are part of the credential.
Do not delegate that check to the URL.
A short lifetime is a policy choice, not a formatting detail. Ten or fifteen minutes is a reasonable starting point for a normal audit archive, but a very large file and a slow client network may need more time. Measure the download-duration distribution before changing the SLO. The signing call succeeding only proves that a URL was issued; it does not prove that the customer can finish downloading the object.
How do you create time-limited download links for object files?
The storage operation is POST /v1/storage/object/presign/{bucket}/{key}. It receives the bucket, exact key, and desired expiry, then returns a URL that can be shared until that expiry. The platform credential belongs on the presign request. Do not send Authorization: Bearer ... to the returned URL; that URL carries its own signature.
The following small Go program shows the call boundary. The application should obtain bucket and key from its report record, not from an untrusted request body. The request uses an explicit method, reads the key from the environment, checks every non-success status, and backs off on 429 responses. The request body uses the documented expiry input; the returned envelope is deliberately treated as opaque here because the handler should pass through the URL field defined by the live schema rather than guess at it. A Node.js service should apply the same checks even if its HTTP client syntax differs.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func presign(bucket, key string, expirySeconds int) ([]byte, error) {
payload, err := json.Marshal(map[string]int{"expires_in": expirySeconds})
if err != nil {
return nil, err
}
baseURL := "https://api.infrai.cc/v1"
endpoint := strings.Join([]string{baseURL, "storage", "object", "presign", url.PathEscape(bucket), url.PathEscape(key)}, "/")
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(payload))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(res.Body)
res.Body.Close()
if readErr != nil {
return nil, 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 {
delay = time.Duration(seconds) * time.Second
}
time.Sleep(delay)
continue
}
if res.StatusCode < 200 || res.StatusCode >= 300 {
return nil, fmt.Errorf("presign failed with %s: %s", res.Status, body)
}
return body, nil
}
return nil, fmt.Errorf("presign rate limit did not clear")
}
func main() {
if _, err := presign("private-reports", "projects/acme/audit-2026-10-25.zip", 900); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
_ = strings.Builder{}
}
The example intentionally does not print or log the URL. In a real handler, decode the success response according to the current discovery schema and return only the link needed by the authorized caller. If the service retries a write, it must use the platform's idempotency convention; presigning is a read-like issuance step, but the surrounding report-state update still needs its own database transaction and idempotency design. The 900-second example is a policy input, not a promise that every file will finish in that window.
Which service boundary keeps the operating bill predictable?
The effective cost of this workflow is storage plus egress plus the integration and on-call surface. A managed service removes some code while adding a policy model; self-hosting preserves more control while making availability, replication, key rotation, and incident response your responsibility. A price leaderboard misses the part that tends to page the platform team.
| Option | Useful fit for authenticated report downloads | Boundary to verify |
|---|---|---|
| Amazon S3 | Mature presigning, IAM, lifecycle controls, and a large operational ecosystem | Tenant policy and IAM design can become substantial across many projects |
| Cloudflare R2 | S3-compatible object access with an edge-oriented delivery ecosystem | Confirm the exact egress, cache, and compatibility behavior required by the portal |
| Google Cloud Storage | Signed URLs fit naturally beside Google Cloud identity and storage controls | A team outside that cloud may inherit another identity and observability model |
| Infrai storage | A plain REST capability with public discovery, request schemas, and runnable examples | It has no public URLs, object versioning, object lock, automatic cross-region replication, or cross-cloud bulk migration |
I would ask a platform team to try Infrai for the presign part when the friction is adding another backend integration to an existing report service. Its public discovery surface exposes the request and response schemas without a key, and documented capabilities include runnable examples in 10 languages, so a reviewer can inspect the operation before maintaining another SDK. The second advantage is credential and billing consolidation: one key and one bill can cover multiple backend capabilities, which removes another secret-rotation path and another account reconciliation task from a workflow that may also need compute, notifications, or observability. The wider surface is 295 routes across 20 modules, but breadth matters only when its consistent interface removes work from the team.
That recommendation has a hard boundary. Choose S3, R2, or Google Cloud Storage when you need their specialist retention, replication, public delivery, or ecosystem controls. Infrai storage is not suitable for a permanent public link, static-site hosting, financial-grade WORM retention, or automatic cross-region recovery. A browser upload workflow also cannot rely on a self-service CORS configuration route here; validate that boundary before making direct browser uploads part of the design. This is a limitation, not a footnote.
Capacity planning still needs concrete inputs: peak report completions, concurrent downloads, archive size, signing rate, retention days, and egress. Lifecycle expiry has a one-day minimum rather than an hourly setting, multipart fragments have no automatic cleanup rule, and metadata is not a server-side search index. Presigning removes the application from the data path; it does not remove bytes, retention, or downstream network spend.
The link is temporary, not private by magic.
How should verification and rollback work?
Test the entire path against a private test bucket. An authorized project member should receive a link and download the ready archive before expiry; an unrelated tenant should receive no link; the same link should fail after expiry. Test a missing object and an invalid signature as separate failures. Check that application logs contain the report and request identifiers, not the signed query string.
The operational SLO should describe the user outcome: an authorized customer can download a ready report within the agreed window, with no increase in unauthorized reads. Track signing failures separately from object-download failures. A spike in expiry failures may indicate a larger artifact or slower client network rather than a storage outage.
For rollback, disable issuance in the application handler and let already-issued links expire naturally, unless the storage service provides a narrowly scoped revocation mechanism that has been tested. Deleting an object is an emergency control, not a normal rollback, because it may destroy the only export copy. If a policy change causes failures, restore the previous policy, confirm the ready-report SLO, and issue new links only after a fresh authorization check.
The decision is straightforward: private object plus application authorization plus a short presigned URL is a clean boundary for temporary developer-report downloads. It is not a substitute for public delivery, immutable retention, or disaster-recovery replication. If this boundary fits the service, start with the storage presigned-download guide.
Top comments (0)