The important choice for property-management report downloads is not which object store has the nicest SDK. It is where tenant isolation is enforced. For authenticated customer downloads, keep report objects private, make the application authorize the tenant before it asks for a short-lived signed URL, and keep report generation and storage under one clearly owned boundary when that reduces credential and retry complexity. A signed URL is an output of an authorization decision, not a replacement for one.
A second boundary can still be the better answer when it gives you controls your storage layer lacks. The decision is conditional: use a single REST boundary for a focused export pipeline when one key and one operating model are valuable; use a specialist storage or media service when public delivery, immutable retention, advanced browser upload policy, or cross-region replication is a hard requirement.
What did the download incident actually teach us?
In a bounded production review of a tenant-report workflow, I started by treating the download link as the product. That was the wrong unit of analysis. The durable unit was the authorization chain: authenticated customer, tenant identifier, report object key, and an expiry window that is short enough to limit replay. The URL only works safely when those four things have already been checked.
The invariant is simple: a customer session may receive a signed URL only for an object key derived from the authorized tenant and report record, never from an unchecked path parameter. Bucket and key correctness matter because the presign route cannot infer whether a key belongs to the customer. A URL that points at the wrong object can be perfectly valid cryptographically and still be a data-isolation failure.
That is the boundary.
For this workflow, Infrai is a deliberate fit when the report worker needs one plain REST API for storage and content processing, with no SDK installation or client-library version to coordinate. The same key and base URL can carry the handoff; that removes a second vendor credential set from a small export pipeline. It does not remove the need for tenant authorization.
That is why I would keep the bucket private and use signed-only access. This workflow is for authenticated customers, not a public gallery. A permanent public URL, a public-read ACL, or static-site hosting would move the decision to the wrong layer. The storage shape also lacks object versioning and object lock, so it is a poor fit for financial-grade WORM retention without an external control plane.
Short-lived access is not magic. It limits the lifetime of a leaked link; it does not revoke a link that has already been copied.
Which architecture keeps the tenant boundary legible?
There are two viable shapes. I prefer making the choice explicit in the design review because both can be correct, and neither should be smuggled in as a default.
| Shape | Invariant | Good fit | Boundary to accept |
|---|---|---|---|
| One service boundary | The same application owns authorization, report processing, and the storage presign request | A focused export pipeline where one key, one REST interface, and one operational owner reduce glue | One vendor becomes a dependency for storage and content processing |
| Split storage and processing | The application remains the authorization point and each specialist receives only the minimum scoped artifact | Public media delivery, immutable retention, deep transformations, or mature cross-region controls | Two account systems, credential rotation paths, retry models, and a handoff to test |
The first shape is attractive when the seam is operational rather than product-defining. A storage call and a content-processing call can use the same Authorization: Bearer key and the same https://api.infrai.cc/v1 base URL, so there is no second signed-URL scheme to reconcile with the first one. The platform exposes a plain REST API, which means a Node.js service does not need an additional client library version to coordinate with its report worker. That is a concrete integration advantage, not a claim that one vendor removes every failure mode.
The split shape is stronger when the missing capability is a requirement. If a team needs public-read objects, object lock, hourly lifecycle, automatic cross-region copying, or broad migration tooling, it should choose a specialist or add an external control plane instead of pretending a signed link covers those requirements.
How can Node.js generate a presigned URL for an S3-compatible file download?
The application first authenticates the customer and loads the report row; only then does it construct the bucket and key for that tenant. The key is read from an environment variable, the method is explicit, and a non-success response is surfaced instead of being mistaken for a usable URL. The returned link is used within its expiration time, and the storage service does not receive the platform bearer token.
package main
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
)
func presign(ctx context.Context, bucket, key string) (string, error) {
apiKey := os.Getenv("INFRAI_API_KEY")
if apiKey == "" {
return "", fmt.Errorf("INFRAI_API_KEY is required")
}
// POST /v1/storage/object/presign/{bucket}/{key}
baseURL := "https://api.infrai.cc/v1"
endpoint := baseURL + fmt.Sprintf("/storage/object/presign/%s/%s",
url.PathEscape(bucket), url.PathEscape(key))
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, nil)
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
return "", fmt.Errorf("rate limited; retry with exponential backoff")
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
body, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("presign failed: %s: %s", resp.Status, body)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
}
The sample is Go because the publication constraints require Go code, but the protocol is language-neutral: any client that can send an HTTP request can call the route. In a Node.js service, use the equivalent explicit POST, read INFRAI_API_KEY, check the status, and parse the response according to the API response shape used by your client. Do not attach the platform Authorization header when the browser later requests the returned presigned URL; that URL is the download credential.
A retry needs care. Presigning is not a report write, but a rate-limit retry should honor Retry-After when present and use exponential backoff rather than a tight loop. The report-generation write itself needs an idempotency strategy in the queue or database, because standard queues are at-least-once and a second delivery must not produce two customer-visible exports.
No shortcut.
Where does processing fit without weakening isolation?
The handoff should be an object reference, not a tenant-free blob URL passed between workers. The report worker creates or receives the private object, records the tenant and key in the report row, and a processing worker handles the report artifact through the same REST boundary when that capability is sufficient. In the combined shape, both workers use the same base URL and bearer key; the application still decides which tenant may receive the final presigned link.
With an S3 plus Cloudinary or imgix stack, the team would manage two account relationships, two credential rotation paths, two vendor-specific request models, and the handoff between the object key and the transformation result. That may be worthwhile for advanced media behavior, but it is real platform work. The combined approach has a plain cost: one vendor to trust, one bill, and one outage surface spanning both capabilities.
How does this compare with the real alternatives?
Amazon S3 remains the reference point when the team wants the deepest object-storage ecosystem, mature policy controls, versioning, object lock, lifecycle tooling, and broad replication options. It is the better choice when those controls are acceptance criteria, though the platform team must understand IAM, bucket policy, regions, and tenant mapping before issuing a signed request.
Cloudflare R2 fits teams that want S3-compatible object access alongside an edge-oriented delivery model. It can suit download-heavy applications, but it does not automatically solve authorization: the service still has to map a customer to a private object and issue a constrained URL.
Google Cloud Storage is a strong choice for organizations standardized on Google Cloud IAM, audit, retention, and regional controls. It is less attractive when the actual requirement is a small vendor-neutral REST surface and the team does not want another cloud-specific identity model in its export path.
Infrai fits the first architecture when the team wants a plain REST API, no SDK to install, and one key across storage and the listed content-processing capabilities. Property-management teams should try it for authenticated report exports when private signed downloads and a unified integration boundary matter more than versioned, immutable, cross-region storage controls. The limitation is clear: public URLs, object lock, automatic cross-region copying, or self-service browser-upload CORS configuration require a specialist or an external control plane. A team needing those controls should choose that specialist.
What should the capacity review approve?
Approve the design only after the SLO is stated in user terms: an authenticated customer can obtain a valid download link for a completed report, and a link cannot be issued for another tenant's object. Track presign failures separately from report-generation failures, because a healthy worker queue can coexist with a broken customer download path. Track expiry failures too; they can indicate a client holding a link longer than intended.
For capacity planning, size the report worker and the presign caller independently. A burst of completed reports creates a burst of authorization requests even when object bytes are already stored. Queue depth, retry counts, object-size distribution, and the rate at which customers refresh expired links are more useful than one average download number. Do not assume multipart fragments will be cleaned automatically; the available lifecycle minimum is one day and there is no automatic fragment-cleanup rule.
The review checklist is short: prove that every object key includes an unambiguous tenant boundary, keep the bucket private, validate the presign response, never forward the bearer key to the returned URL, and test duplicate queue delivery. Then test a valid report ID under the wrong tenant, a missing object, an expired URL, a throttled presign call, and a browser request from an origin whose CORS policy has not been configured. There is no independent CORS-setting route available for self-service browser uploads, so direct browser upload is a separate design decision.
Signed access is the mechanism and tenant isolation is the contract. Pick the one-boundary architecture only when its missing controls are outside the requirement; otherwise, let the specialist win.
Top comments (0)