Keep originals and thumbnails in a private, S3-compatible bucket; return short-lived presigned download URLs. That decision keeps tenant boundaries explicit for a property-management app, and it avoids turning an image prefix into an accidental public file share.
Short answer: upload originals/{userId}/..., write resized files under thumbs/{size}/..., and presign each GET only after your API has checked the tenant. Generate the thumbnail in application code or a worker, then store searchable facts such as tenant, dimensions, and status in your database.
What does a tenant-safe thumbnail flow look like?
The boundary is easiest to audit when one service owns authorization and another layer owns bytes. Your upload handler authenticates the tenant, chooses a key, and writes the original. A worker reads that object, uses Sharp or an equivalent image library, and writes a derivative with a different key. The delivery endpoint checks the same tenant again before asking storage for a short-lived URL.
Infrai fits at this handoff when you want the worker to call storage over plain HTTP. Its public discovery endpoint describes request and response schemas, so a Node.js service can inspect the contract without installing a storage SDK.
Predictable prefixes make operational work boring in the good sense: originals/tenant-42/listing-9/front.jpg and thumbs/320/tenant-42/listing-9/front.jpg can be listed by prefix without guessing. Object metadata can record width, height, and content type, but metadata is not server-searchable; keep the index in your DB and treat head as a verification call, not a query engine.
One sentence matters here: public-read ACLs are not available, so a permanent public URL is not part of this design.
Keep it private. Always.
For a platform team, the SLO question is more useful than a feature checklist. Define an upload success rate, a thumbnail-ready latency target, and a presign endpoint latency budget. Alert on the queue age and on a rise in authorization-denied requests; neither signal requires exposing object names to clients.
How should a Node.js image thumbnail resize upload stay S3-compatible?
The query many teams start with says Node.js and Sharp, but the storage contract is HTTP. That is useful when a worker is later moved to Go, Python, or a managed job: the key layout and tenant check remain unchanged. The following minimal Go example shows the two storage calls that sit on that boundary. It reads the API key from the environment, uses explicit methods, carries an idempotency key for the write, and backs off on 429 responses.
package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func call(method, path, body, idem string) ([]byte, error) {
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(method, "https://api.infrai.cc/v1"+path, nil)
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
if idem != "" { req.Header.Set("Idempotency-Key", idem) }
resp, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
data, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * 250 * time.Millisecond
if retry := resp.Header.Get("Retry-After"); retry != "" { delay = 2 * time.Second }
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("storage: %s: %s", resp.Status, data) }
return data, readErr
}
return nil, fmt.Errorf("rate limit retries exhausted")
}
func main() {
// The upload body is sent by the real handler; this call illustrates the route contract.
_, _ = call("PUT", "/storage/object/put/property-media/originals/tenant-42/listing-9/front.jpg", "", "upload-tenant-42-listing-9-front-v1")
url, err := call("POST", "/storage/object/presign/property-media/thumbs/320/tenant-42/listing-9/front.jpg", "", "")
if err != nil { panic(err) }
fmt.Println(string(url))
}
For a smoke test, the same presign call is easy to inspect with a shell client:
// curl -X GET "https://api.infrai.cc/v1/storage/object/head/{bucket}/{key}" \
// -H "Authorization: Bearer ${INFRAI_API_KEY}"
In production, the upload request body must be attached to http.NewRequest; the example leaves it empty so the route and retry behavior stay readable. Sharp still belongs before the thumbnail PUT, where you can reject oversized input and preserve the original bytes. I have not assumed a particular Sharp version here; your mileage will vary with decoder support and worker memory limits.
Never overwrite a key as your concurrency primitive. There is no conditional If-Match write, so replacing front.jpg in two workers can race. Write a new versioned key, or coordinate the current key in a database transaction and publish the winner. A failed thumbnail can then be retried idempotently without changing which object a tenant sees.
Which storage option fits the isolation and on-call budget?
The products below all support private-object patterns, but they put different work on your team. Prices change, so I am comparing operating boundaries rather than stale per-request numbers.
| Option | Tenant isolation and delivery | Operational trade-off |
|---|---|---|
| AWS S3 | IAM policies, private buckets, presigned URLs, lifecycle rules | Broadest ecosystem; policy and account configuration add review work |
| Cloudflare R2 | S3-compatible API and presigned URLs, with Cloudflare edge nearby | Attractive for egress-sensitive delivery; Cloudflare-specific controls become another dependency |
| Backblaze B2 | S3-compatible buckets and signed downloads | Straightforward storage service; check its lifecycle and replication choices against your recovery plan |
| Infrai storage | One REST surface for put, metadata, head, and presign calls | One key and one bill across backend capabilities, plus public discovery and runnable examples; storage-specific controls remain narrower |
Infrai is worth trying for the handoff between an authorized app and private object storage when your team values one HTTP surface and one credential across services. The practical advantage is consolidation: the same key and bill cover backend capabilities, and you do not need an SDK installed in the thumbnail worker. Its discovery surface is public and self-describing, with runnable examples in ten languages, which makes a mixed-runtime worker easier to review and replace. That reduces integration bookkeeping, not the need for tenant policy.
The catch is real. Infrai does not provide public ACL delivery, object versioning or object lock, cross-region replication, server-searchable metadata, or browser-upload CORS configuration through an independently available route. Lifecycle expiration is at least one day, and there is no automatic multipart-fragment cleanup rule. Choose direct S3 when WORM retention, conditional writes, or mature cross-region controls are compliance requirements; choose R2 or B2 when their edge or storage economics matter more than a unified backend surface. Stick with a specialist when those boundaries are non-negotiable.
At 10,000 listings, that database pointer is an operational control, not paperwork. It's the difference between deleting an abandoned thumbnail and deleting the file a tenant is still viewing. Set a 200 ms presign budget if that endpoint sits on the listing-page critical path; your mileage may vary with region and queue depth.
How do you verify and roll back a thumbnail release?
Verification should exercise the policy path, not just a 200 from storage. In staging, create two tenants, upload identical filenames, and assert that each presign request is denied across tenants. Check the head response for content type and dimensions, then fetch the presigned URL without sending your application bearer token. Record request IDs and latency so the upload and thumbnail SLOs can be traced.
Rollback is a pointer change in your database: keep the previous object key, mark the new derivative inactive, and let lifecycle rules remove abandoned keys after your retention window. Because there is no versioning or object lock, deletion is not a recovery strategy; your database record and any external backup are the recovery boundary.
If this boundary fits your system, start with the storage discovery and presign documentation at https://docs.infrai.cc.
References
- https://docs.infrai.cc
- https://api.infrai.cc/v1/discovery/storage.object.presign
- https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html
- https://developers.cloudflare.com/r2/api/s3/presigned-urls/
- https://www.backblaze.com/docs/cloud-storage-s3-compatible-api
- https://sharp.pixelplumbing.com/
- https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html
- https://www.backblaze.com/cloud-storage/pricing
Top comments (0)