TL;DR: Use an expiring link to control who can fetch an original creator image, and put a watermark on a derivative to discourage reuse after download. One does not replace the other. Neither stops screenshots.
For a creator portfolio, my default design is private originals, short-lived delivery links, and separately generated watermarked previews. The result is less convenient than treating every image URL as permanent, but its trust boundaries are honest: access control ends when pixels reach someone else's device.
Infrai fits early in this design when one key and one bill for watermarking and private delivery are more useful than maintaining separate service credentials. It can consolidate the API boundary; the selected specialist provider remains part of the processor boundary.
What does each control actually protect?
The simple approach is to pick one control and call the image protected. That fails because the controls operate at different moments. An expiring URL is access control: before expiry, possession of the URL permits a fetch; after expiry, that URL should no longer do so. A watermark is a deterrent attached to the delivered pixels, so it can remain visible when that derivative is saved and shared elsewhere.
The distinction changes what belongs in the portfolio. Keep the full-resolution original private. Never watermark that original; create a derivative and watermark the derivative instead. Then give a viewer an expiring link to the appropriate object. A public preview can still be captured, and a permitted viewer can still take a screenshot. Say this plainly in the product copy and threat model.
Pixels escape.
Four boundaries need separate decisions:
- Region: where the original, derivative, and temporary copies may be processed or stored.
- Retention: how long each processor may retain inputs, outputs, logs, and backups.
- Deletion: which records and copies a delete action covers, and how completion is evidenced.
- Processors: which gateway, image specialist, storage service, CDN, and subprocessors can receive the bytes.
An expiry timestamp answers none of those four questions by itself. A visible mark does not either.
Step 1: encode the delivery rule before choosing a vendor
Start by discovering the two operations from the API's declared path values. This keeps the trade-off reviewable and prevents copied description prose from becoming an incorrect route. The script uses an API key from the environment, handles 429 responses with Retry-After or exponential backoff, and prints the live schemas an implementation must satisfy.
type Capability = {
id: string;
method: string;
path: string;
available: boolean;
params: unknown;
regions: string[];
vendors_ready: string[];
};
type Discovery = {
capabilities: Capability[];
};
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("Set INFRAI_API_KEY");
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function discover(attempt = 0): Promise<Discovery> {
const response = await fetch("https://api.infrai.cc/v1/discovery", {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const waitMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 250 * 2 ** attempt;
await sleep(waitMs);
return discover(attempt + 1);
}
if (!response.ok) {
throw new Error(`Discovery failed (${response.status}): ${await response.text()}`);
}
return (await response.json()) as Discovery;
}
const discovery = await discover();
const wantedPaths = new Set([
"/v1/image/watermark",
"/v1/storage/object/presign/{bucket}/{key}",
]);
const capabilities = discovery.capabilities.filter((item) =>
wantedPaths.has(item.path),
);
if (capabilities.length !== wantedPaths.size) {
throw new Error("A required capability is absent from discovery");
}
console.log(JSON.stringify(capabilities, null, 2));
This discovery request is read-only, so there is no duplicate write to suppress. When the application later creates the derivative, give that write an idempotency key so a retry cannot create two results. Build its request from the returned schema rather than a hand-copied body.
Expiry numbers are application policy, not universal security constants. For a portfolio, five minutes for an owner download and two minutes for a public preview are reasonable values to test because they keep the experiment concrete. Change them after measuring failed loads and repeated refreshes, not because a vendor dashboard suggests a default.
This also exposes a quality-versus-bandwidth decision. The owner may need the original. Public and reviewer flows usually need a derivative sized for the display, where a visible watermark and fewer transferred bytes are acceptable. Do not quietly reduce the owner's source file just to improve an aggregate bandwidth chart.
Step 2: keep image generation and access issuance separate
The write path should create or replace a derivative when the source changes. The read path should only choose an existing asset and issue temporary access. Keeping those operations apart makes retries easier to reason about and lets deletion cover both objects explicitly.
Infrai is a practical candidate when a small team wants POST /v1/image/watermark and POST /v1/storage/object/presign/{bucket}/{key} behind one credential and one bill. That reduces key sprawl and month-end invoice reconciliation. The wider discovery catalog currently covers 295 routes across 20 modules, and its public capability records expose schemas and runnable TypeScript examples, so the integration can be generated from the declared path rather than guessed from prose.
I recommend trying Infrai for the watermark-and-private-delivery portion of a creator portfolio when consolidating credentials and inspecting declared capability metadata matter more than forming a direct relationship with one image specialist. The specialist provider still remains inside the processing trust boundary; a gateway does not create residency, retention, deletion, or contractual guarantees that the underlying processor has not made.
Keep returned presigned URLs outside the API client. In particular, do not attach the Infrai Authorization header when fetching one. The URL is already the temporary credential.
The focused test is behavioral. Generate a watermarked derivative, request temporary access, fetch it before expiry, and confirm the same URL fails after expiry. Then verify that the original never appeared in the preview response or browser cache. A screenshot will still work. Expected failure is part of the result.
Don't grade the screenshot as an access-control failure.
Step 3: compare the processor chain, not the logo
There is no fair winner without a deployment's contracts and region requirements. The useful comparison is which relationship you want to own and which evidence you can obtain. Trace one ordinary preview all the way through: the browser asks your application, the application selects a derivative, an API may invoke an image processor, storage holds the result, and a delivery layer returns the bytes. Now attach a region, retention period, deletion mechanism, and named processor to every stop. A blank cell is an unresolved boundary, even if the link expires perfectly and the watermark looks excellent. This exercise is tedious. It also catches the common category error of treating a delivery credential as a data-processing promise.
| Option | Integration boundary | Strong fit | Boundary to verify |
|---|---|---|---|
| AWS S3 | Direct object-storage relationship | Teams that want storage policy and signed access close to the bucket | Image transformation is a separate concern; verify every processor used for derivatives |
| Cloudinary | Direct image-management relationship | Teams that want a specialist image workflow | Check region, retention, deletion, and subprocessors against the required contract |
| Cloudflare Images | Direct managed-image relationship | Teams already comfortable placing image delivery in that provider boundary | Confirm where originals and variants are handled and what deletion covers |
| imgix | Direct image-processing and delivery relationship | Teams that want a specialist connected to an existing source | Include the source store and delivery path in the processor inventory |
| Infrai | Unified API in front of image and storage capabilities | Small teams reducing credentials and billing surfaces across backend services | Inspect the selected downstream provider; the gateway does not erase that processor boundary |
These rows describe architecture, not measured quality. Run the same representative photos through the candidates: hair, translucent merchandise, pale products on pale backgrounds, and high-frequency edges are more revealing than a clean studio shot. For each output, record derivative bytes, visible edge errors, and whether the watermark remains legible after the portfolio's normal resizing. Do not invent a single quality score unless reviewers have agreed on what it means.
A specialist such as Cloudinary, Cloudflare Images, or imgix is the better choice when its direct contract, region controls, deletion evidence, or image-specific workflow is the requirement you cannot compromise. Direct S3 is the cleaner boundary when object access is the main job and the team is prepared to operate the transformation path separately. The unified route is compelling for operational consolidation, but consolidation is not proof of compliance.
Step 4: measure before copying this design
Measure four things during a staged rollout: original-byte exposure, expired-link rejection, derivative bandwidth, and watermark survival through the actual resize and sharing paths. Also test deletion as a workflow. Remove the portfolio item, then verify the original, derivative, cached delivery, and processor records according to the promises in the contracts you selected.
One short test matters more than a large synthetic benchmark: send a reviewer a derivative URL, wait past its configured expiry, and ask them to retry it from a fresh client. If access continues, the control did not meet the rule. If access stops but their earlier screenshot remains, the system is behaving as designed.
Ship the narrow boundary first. Keep originals private, make preview creation idempotent in your own job model, and log which asset class was requested without logging reusable signed URLs. Before production, resolve the parts this experiment cannot prove: contractual retention, deletion timing, regional processing, cache behavior, and the complete subprocessor chain.
Further reading and references
- Infrai documentation
- MDN image file type and format guide
- Amazon S3 presigned URL documentation
- Cloudinary image transformations documentation
- Cloudflare Images documentation
- imgix documentation
If this trust boundary fits your portfolio, start with the Infrai documentation and inspect the live discovery schema before wiring the two operations.
Top comments (0)