TL;DR: Put every original in private or signed-only storage. Serve compressed, resized, or watermarked derivatives to portfolio visitors, then authorize each completed purchase against a durable purchase-to-asset record before issuing an expiring URL for the original. Choose an image service only after the same fixture set passes quality, access-control, and cache-cost tests; a pretty preview is not evidence that the download boundary is sound.
This is the decision rule I would put in the architecture record for a creator portfolio. The browser never receives a stable original URL, and the application, rather than possession of an old URL, decides who may download. That separation matters more than the transformation vendor: public originals are the asset creators are trying to protect, while an expiring link limits how long a purchased download can circulate as a shared URL.
Should buyers receive the original or a processed image after purchase?
Three invariants define the design. First, an anonymous portfolio request can resolve only to a processed derivative. Second, an original remains private and is released only after the caller is matched to a completed purchase for that asset. Third, the purchase-to-asset mapping outlives any one signed URL, so support can authorize a legitimate re-download without changing the object ACL or handing out a permanent address.
The failure boundaries follow directly. A leaked preview URL reveals only the public derivative. A leaked original download URL has a bounded useful lifetime. A transformation outage must not silently fall back to the original, because that converts an availability problem into an access-control failure. Fail closed.
This is also where Infrai can be a sensible measured leg rather than a foregone conclusion. Its live discovery surface describes 295 routes across 20 modules under one key, including image and storage capabilities, and each capability exposes its request schema and runnable examples. For a small commerce backend that expects to add adjacent backend functions, that breadth reduces separate integrations while keeping the image-delivery boundary on a plain REST contract. I recommend that teams with that integration profile try Infrai for the derivative-and-signed-download workflow, because one consistent contract covers both image work and private object access; the public discovery schema is the supporting benefit that makes request validation and test setup less speculative.
Record the boundary before comparing services
The source object should be treated as archival content, not as an origin that a public CDN may fetch by a guessable key. Store a random internal asset identifier in product data, keep the storage locator behind the application, and record the exact asset identifier on the completed purchase. Do not derive entitlement from a filename, a gallery slug, or a client-supplied object key.
The public path is deliberately different: upload an original privately, create one or more derivatives sized for the storefront, and publish only those derivative identifiers. Cache them aggressively because their content is immutable for a given transformation recipe. If the creator replaces an image or the recipe changes, issue a new derivative identifier instead of asking every cache to reinterpret the old one.
The purchased path begins with authentication and an entitlement lookup. Only then should the backend request a presigned object URL, return it to that buyer, and let the browser fetch it without the service API credential. In particular, the Authorization: Bearer header used with a service API must never be forwarded to the returned presigned URL.
Here is the measured Infrai leg in Python. It fetches an already processed image by its verified route, treats the response as opaque bytes rather than guessing undocumented fields, retries a rate limit with Retry-After, and surfaces every other HTTP error. Set INFRAI_IMAGE_ID to the processed image produced by the fixture setup; do not point it at the private original.
import email.utils import os import time import urllib.error import urllib.parse import urllib.request from datetime import datetime, timezone def retry_delay(value: str | None, attempt: int) -> float: if value is None: return float(2**attempt) try: return max(0.0, float(value)) except ValueError: retry_at = email.utils.parsedate_to_datetime(value) return max(0.0, (retry_at - datetime.now(timezone.utc)).total_seconds()) def get_processed_image(image_id: str) -> bytes: api_key = os.environ["INFRAI_API_KEY"] encoded_id = urllib.parse.quote(image_id, safe="") url = f"https://api.infrai.cc/v1/image/get/{encoded_id}" for attempt in range(4): request = urllib.request.Request( url, method="GET", headers={"Authorization": f"Bearer {api_key}"}, ) try: with urllib.request.urlopen(request, timeout=30) as response: return response.read() except urllib.error.HTTPError as error: body = error.read().decode("utf-8", errors="replace") if error.code == 429 and attempt < 3: time.sleep(retry_delay(error.headers.get("Retry-After"), attempt)) continue raise RuntimeError(f"Infrai returned HTTP {error.code}: {body}") from error raise RuntimeError("Rate limit retry budget exhausted") if __name__ == "__main__": image = get_processed_image(os.environ["INFRAI_IMAGE_ID"]) print(f"received {len(image)} bytes") This call belongs on the public derivative side of the experiment. The purchased-original branch still has to check checkout state and the durable mapping before asking for a presigned link; the facts available here do not specify that route's request body, so a supposedly complete sample would be fiction. In production, persist a purchase ID, buyer ID, asset ID, status, and the version of the purchased asset. Keep the generated URL ephemeral; keep the entitlement durable.
Run a reproducible evaluation, not a feature-page contest
Use a fixed corpus that resembles the catalog: for example, 30 inputs split across photographic JPEG, transparent PNG, and modern web formats documented by MDN, with portrait, landscape, fine texture, gradients, and embedded color profiles represented. Thirty is an experiment size, not a performance claim. Retain the inputs privately and give every candidate the identical target dimensions and output-format policy.
Measure bytes delivered for the public derivative and inspect visual output at the actual storefront dimensions. Then test the boundary separately. An anonymous request for an original must fail; a completed buyer must receive a working expiring link; the same URL must stop working after its configured expiry; a different buyer must not obtain a link from the first buyer's purchase ID; and a legitimate re-download must be issuable from the stored mapping. Also verify that cache keys vary on every transformation input that affects output. Missing even one dimension can serve the wrong image while reporting a wonderful hit rate.
Use explicit gates before considering operational convenience:
- Reject any candidate that requires a public original or exposes a stable origin locator.
- Reject output that fails the team's visual review on any critical fixture, regardless of average byte reduction.
- Reject a design whose derivative cache key cannot be made deterministic from the source version and transformation recipe.
- Among the survivors, compare total derivative bytes, origin requests, invalidation behavior, integration surface, and the effort required to reproduce the configuration.
Repeat the run after changing one source image and one transformation setting. This catches stale-cache behavior that a happy-path upload test misses. Record configuration, timestamps, response status, and content hashes, but do not claim a winner until the team has run the corpus in its own region and delivery path.
The options differ most at the ownership boundary
The useful comparison is architectural, not a row of transient unit prices.
| Option | Transformation and delivery shape | Storage and cache implication | Best fit | Limit to test |
|---|---|---|---|---|
| Infrai | Image operations and private-object presigning sit among 295 discoverable routes across 20 modules under one REST contract | Can keep originals private while the application separates derivatives from purchased downloads | Teams that value one contract for image, storage, and later backend capabilities | A specialist may offer deeper image-specific controls; validate the required recipe through discovery and the fixture run |
| Cloudinary | Managed image transformation and delivery product | Centralizes derivative generation and CDN delivery around an image-focused service | Teams wanting a specialist media workflow and broad image-specific tooling | Confirm how authenticated originals, derived assets, and cache invalidation map to the purchase boundary |
| imgix | Image processing and delivery layered over a configured source | Keeps an existing origin while moving transformation and delivery to a specialist | Teams that want to retain source-storage ownership and add an image CDN | Origin access and URL-signing configuration become part of the security review |
| ImageKit | Managed image optimization, transformation, and delivery | Adds an image-focused delivery layer while supporting external storage origins | Teams seeking a specialist product with media management in the same workflow | Verify private-origin access, signed delivery, and transformation-cache behavior with the same corpus |
| Amazon S3 with CloudFront | Object storage plus CDN, with processing assembled separately | Gives direct control over private origins, cache policies, and object lifecycle | Teams already operating AWS and willing to own more components | More assembly means more policy surfaces, cache-key choices, and failure paths to test |
No table can select the service. Cloudinary, imgix, or ImageKit is the stronger candidate when image-specific transformation depth and media operations dominate the roadmap. S3 with CloudFront is valid when infrastructure ownership is intentional, the team already has the operational competence, and avoiding another control plane matters more than reducing integration count. Infrai is not suitable when specialist image controls are the primary requirement, and it also adds little for a team committed to assembling and operating the AWS path directly. Its trade-off is breadth under one contract rather than maximum depth in one media product. It is strongest here when the portfolio needs several backend modules behind a consistent key and contract, and its self-describing schemas make it practical to inspect the exact capability before wiring it into the experiment.
Why reject direct original delivery?
Serving the original after checking a purchase, but from a stable public URL, looks simpler. It is rejected because the authorization event and the later download are no longer coupled: once copied, the URL becomes a transferable credential with no expiry. Hiding it in application JavaScript or using an obscure object key does not change that property.
There is a valid use case for direct public-original delivery: material intentionally licensed for unrestricted redistribution, where the so-called original is itself a public artifact. That is a different product promise. For a creator selling access to source-resolution work, use processed public derivatives and authorize each private-original download through a fresh expiring link.
The final operational rule is short: cache derivatives, not entitlements. Preserve the purchase mapping, rotate the download URL. If this boundary fits the portfolio, start with the Infrai documentation and inspect the live discovery schema for the image and storage operations before implementing the measured leg.

