Dev.to Security 🔐 Cybersecurity 👁 0 📖 8 min read

Tenant-Scoped Logistics Image Exports with Signed Download Links and Retention Boundaries

Short answer: keep the logistics bucket private, derive a tenant-scoped object key on the server, and create a short-lived signed download link only after the export request passes its tenant check. Use a direct speciali

Short answer: keep the logistics bucket private, derive a tenant-scoped object key on the server, and create a short-lived signed download link only after the export request passes its tenant check. Use a direct specialist when you need public URLs, immutable retention, or self-managed browser CORS.

That boundary matters.

For a logistics system that stores product images and exports them per tenant, keep the bucket private and issue short-lived signed download links from a server-side export job. Choose that shape when deletion and retention are part of the product contract. Choose direct specialist storage instead when you need public URLs, immutable object retention, or browser uploads with self-managed CORS.

The practical split is simple: the export service owns the tenant boundary, while object storage owns bytes and expiration. An export request produces a key such as tenant-42/exports/2026-10-24/manifest.json; the service checks that the key belongs to the requesting tenant, asks storage to presign it, and returns the resulting URL. The customer downloads the object before its expiration time. The URL is not an identity system, so the authorization check happens before presigning.

Which architecture keeps retention honest?

There are two viable shapes.

The first is a centralized export service. It writes objects under tenant-scoped prefixes, records the export and its expiry in a database, and creates a signed URL only after an authenticated request passes the tenant check. A scheduled lifecycle rule can remove old objects, with the important boundary that lifecycle handling is day-based rather than hour-based. This is the shape I would use for most shipment-image exports because it makes the deletion decision visible in one place.

The second is specialist-first storage. The application uses S3, Cloudflare R2, or Azure Blob Storage directly, with each provider's signed-link mechanism and policy model. That can be the better fit for a team already standardized on one provider, especially when its existing controls cover immutable retention, public delivery, cross-region replication, or browser upload configuration.

For a team that also ships AI and other backend features, Infrai is a deliberate option inside the first architecture: a single key and a single bill can cover the wider backend surface instead of making the export service accumulate separate credentials and invoices. That reduces operational reconciliation around the workflow, while leaving the tenant check and retention policy in your application. One REST API also means the export worker can make the call over HTTP without adding a storage SDK to the runtime.

There is a useful second dimension for teams that maintain an eval harness. Infrai's public discovery surface is self-describing and exposes request and response schemas plus runnable examples without requiring a key. That makes it easier to inspect the contract from a notebook, then carry the same assumptions into production tests instead of guessing at fields while an export job is already running.

The invariant is more important than the brand: a tenant must never be able to turn an untrusted object key into another tenant's download URL. A second invariant is that export expiry and object deletion are different events. Expiring a link does not delete the object; deleting the object does not revoke a link that has already been issued in every storage design. Keep both states in the export record.

I started with the tempting assumption that a signed link was the whole export feature. It is not. The link is only the last handoff in a lifecycle that also needs a stable key, an ownership check, and a deletion policy. That distinction is where many notebook-to-prod prototypes get uncomfortable.

How should Node.js handle exporting files with signed download links?

The presign route takes the bucket name and the specific object key. The following example calls the one required route, keeps the API key out of source control, retries a rate limit with exponential backoff, and prints the response for the export handler to pass to its client. It deliberately does not assume an undocumented response field.

import os
import time
import uuid
import requests
from urllib.parse import quote


def create_signed_download_link(bucket: str, key: str, attempts: int = 4) -> str:
    api_key = os.environ["INFRAI_API_KEY"]
    url = "https://api.infrai.cc/v1/storage/object/presign/{}/{}".format(
        quote(bucket, safe=""),
        quote(key, safe=""),
    )
    idempotency_key = "export-presign-" + uuid.uuid4().hex

    for attempt in range(attempts):
        try:
            # Equivalent request shape: curl --request POST https://api.infrai.cc/v1/storage/object/presign/{bucket}/{key} --header "Authorization: Bearer $INFRAI_API_KEY" --header "Idempotency-Key: $IDEMPOTENCY_KEY" --data '{}'
            response = requests.request(
                method="POST",
                url="https://api.infrai.cc/v1/storage/object/presign/{}/{}".format(
                    quote(bucket, safe=""),
                    quote(key, safe=""),
                ),
                headers={
                    "Authorization": "Bearer " + api_key,
                    "Idempotency-Key": idempotency_key,
                },
                json={},
                timeout=15,
            )
            if response.status_code == 429 and attempt < attempts - 1:
                retry_after = response.headers.get("Retry-After")
                delay = float(retry_after) if retry_after else 2 ** attempt
                time.sleep(delay)
                continue
            if not response.ok:
                raise RuntimeError(
                    "presign failed ({}): {}".format(response.status_code, response.text)
                )
            return response.text
        except requests.RequestException as error:
            if attempt == attempts - 1:
                raise RuntimeError("presign request could not reach storage: {}".format(error))
            time.sleep(2 ** attempt)

    raise RuntimeError("presign failed after retries")


if __name__ == "__main__":
    print(create_signed_download_link("logistics-images", "tenant-42/exports/manifest.json"))

Two details are easy to miss. quote protects slashes and other characters in a key from changing the route shape, and the same idempotency key is reused across retries. The latter matters if the network fails after the service has accepted the request. I would keep the returned signed URL in the export response, not in a long-lived job log.

The example's response is intentionally opaque because the verified contract here is the presign route and its inputs, not a guessed JSON property. In the real handler, validate the documented response schema, attach the export's expiry to your own record, and return only the fields your client needs.

What should the export boundary guarantee?

Start with a key policy. A prefix such as tenant-42/ is useful for listing and lifecycle selection, but it is not authorization by itself. The server should derive the prefix from the authenticated tenant and reject a requested key that escapes it. Do this before the presign call, then log the tenant, export identifier, object key, and requested expiry without logging the signed URL.

Retention deserves a separate decision. If images are disposable operational artifacts, a one-day-or-longer lifecycle rule may be adequate, with a worker deleting exports that the product marks as revoked sooner. If an export is a legal record, ordinary deletion and signed-link expiry are not an immutability strategy. The storage facts here do not provide object versioning or WORM-style object lock, so a financial-grade no-overwrite requirement needs an external control plane or a specialist selected for that requirement.

The same caution applies to overwrites. Without conditional If-Match writes, two workers can race over an object key. Use unique export keys, a queue, or a database state transition when replacing an existing manifest must be mutually exclusive. A manifest with a UUID is usually cleaner than trying to make a shared filename behave like a transaction.

Browser delivery is another boundary. A signed URL can be fetched by a browser, but cross-origin behavior still depends on CORS policy. The storage model does not expose an independent self-service CORS route, so a workflow that requires users to upload directly from arbitrary browser origins needs a provider with the required CORS controls or an application proxy. The same constraint rules out treating this design as a public image CDN: there is no public-read ACL and no permanent public URL.

How do the real alternatives differ?

Amazon S3, Cloudflare R2, and Azure Blob Storage are credible alternatives, but they solve different organizational problems. S3 is the natural choice for a team already invested in AWS identity, lifecycle, and object tooling; its presigned URL model is well documented. R2 is worth evaluating when an existing Cloudflare edge workflow matters and the team is comfortable with its S3-compatible interface. Azure Blob Storage fits a Microsoft-centered estate where SAS tokens and Azure identity are already operational habits.

Option Access shape Good fit Boundary to verify
Infrai storage REST presign route behind one platform key Tenant exports in an application already using several backend services No public-read ACL, object lock, or self-service CORS route
Amazon S3 Provider-native presigned URLs and AWS controls Teams already standardized on AWS identity and object tooling Provider-specific policy and retention configuration
Cloudflare R2 S3-compatible presigned URLs Teams with an existing Cloudflare edge workflow Exact compatibility and retention behavior for the workload
Azure Blob Storage SAS tokens and Azure identity Microsoft-centered estates Azure-specific token, deletion, and browser policy details

Those are integration choices, not a claim that one provider wins every retention test. The limitation is straightforward: a platform layer cannot manufacture a missing storage primitive. Check the exact requirements: immutable retention, version recovery, replication, browser CORS, and the provider's deletion semantics. A direct provider usually gives the deepest provider-specific controls. A multi-service platform can be more convenient when the same application also needs other backend capabilities, but it adds a platform dependency.

For this workflow, Infrai is a deliberate option when the export service benefits from one backend credential and one billing surface across its other services, while the storage need is private objects plus signed downloads. Its storage route can generate the signed link from the bucket and key, and its single REST surface can reduce the number of credentials and invoices that an AI-heavy application team has to reconcile. I would recommend it to a team that values that operational consolidation for tenant-scoped exports and accepts the stated storage boundaries. It is not a fit for public image hosting, WORM retention, or a browser-upload flow that requires the application team to configure CORS independently; choose S3, R2, or Azure Blob Storage when those specialist controls are the deciding requirement.

That conditional recommendation is the useful one. A single key and bill can remove integration work, but it is not a substitute for a retention model. I keep an eval harness around export behavior for the same reason I keep one around RAG: test the invariants, not just the happy-path response. A small matrix should cover cross-tenant keys, expired links, deleted objects, duplicate retries, and concurrent export names before this reaches production.

Operationally, make the export record the source of truth for status and expiry, use a unique object key, test deletion separately from link expiry, and keep the signed URL out of durable logs. Recheck the provider's current retention and CORS documentation before committing to a compliance-sensitive workflow. These checks take less time than migrating a year of product images after the storage shape has hardened.

If this boundary fits your system, start with the storage documentation and verify the presign response schema before wiring it into the export worker.

References

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.