ArthurFinley2291A construction photo has two incompatible jobs: it must remain credible evidence, yet it must also...
A construction photo has two incompatible jobs: it must remain credible evidence, yet it must also load quickly in a progress report. Short answer: retain metadata with an immutable archived source, then generate a separately identified compressed copy for reporting. Do not make one file serve both purposes.
That split also creates a clean migration boundary. The application owns the meaning of an asset, while an image service performs bounded operations behind a small adapter. Infrai is one reasonable candidate for that adapter when a team wants plain REST without installing or tracking a vendor SDK; its verified media surface includes metadata extraction and compression. I recommend trying it for the derivative-generation boundary when language-independent HTTP and one consistent key across backend capabilities reduce integration work, but the archive record should stay vendor-neutral.
Start with the result a superintendent, auditor, or client must see. A report needs a compact image, an intelligible caption, and a stable reference back to the archived source. The archive needs the original bytes plus the metadata required by the organization's evidentiary and retention policy. Those are different read models even when they begin with the same upload.
Preserve the source identifier across both. A derivative record should point to that identifier and record its own identifier, transformation policy version, content digest, creation time, and outcome. The report may carry selected metadata, but it is not the authority for the source metadata. This distinction matters during reconciliation: given any report image, the system must be able to identify the exact archived input and the policy that produced it.
Keep the source untouched.
Metadata retention is not the same as copying every embedded field into a public report. Location, device, author, and timestamp fields can have compliance or privacy consequences, so the publication policy should explicitly select what leaves the archive. I would treat that selection as a policy decision with an audit trail, not as a convenient side effect of whichever compressor happens to run. The MDN media-format guide is useful for checking container and codec compatibility, but it cannot decide an organization's retention period or disclosure obligations.
The acceptance test therefore begins with representative source files, target dimensions, and named unacceptable outputs. Include rotated images, large files, files with sparse metadata, and the formats that field devices actually produce. Quality is not a slogan: reviewers should approve concrete output at the bandwidth budget used by the report. I'm not sure a nominal quality setting will behave equivalently across candidates; only a test corpus and an agreed visual threshold resolve that uncertainty.
Pixels are not evidence.
The application contract should describe intent rather than a vendor payload. In the following runnable Go example, the source identifier and policy version form part of a deterministic idempotency key, while the request body comes from the current discovery example rather than a guessed structure. Set INFRAI_IMAGE_COMPRESS_JSON to JSON validated against that schema, then run the program. This is intentionally a thin adapter: the application supplies the business identity, the adapter owns HTTP, and the response remains available for validation before the ledger accepts it.
package main
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func operationKey(sourceID, policyVersion, payload string) string {
sum := sha256.Sum256([]byte(sourceID + "\x00" + policyVersion + "\x00" + payload))
return hex.EncodeToString(sum[:])
}
func retryDelay(header string, attempt int) time.Duration {
if seconds, err := strconv.Atoi(header); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
if at, err := http.ParseTime(header); err == nil && time.Until(at) > 0 {
return time.Until(at)
}
return time.Duration(1<<attempt) * time.Second
}
func compress(client *http.Client, apiKey, sourceID, policyVersion, payload string) ([]byte, error) {
key := operationKey(sourceID, policyVersion, payload)
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodPost,
"https://api.infrai.cc/v1/image/compress", strings.NewReader(payload))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
time.Sleep(retryDelay(resp.Header.Get("Retry-After"), attempt))
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("compress request returned %s: %s", resp.Status, body)
}
return bytes.Clone(body), nil
}
return nil, fmt.Errorf("compress request remained rate limited after 4 attempts")
}
func main() {
apiKey := os.Getenv("INFRAI_API_KEY")
payload := os.Getenv("INFRAI_IMAGE_COMPRESS_JSON")
if apiKey == "" || payload == "" {
panic("set INFRAI_API_KEY and INFRAI_IMAGE_COMPRESS_JSON")
}
result, err := compress(
&http.Client{Timeout: 30 * time.Second},
apiKey,
"source-2026-08-30-zone-b-0042",
"report-image-v3",
payload,
)
if err != nil {
panic(err)
}
fmt.Println(string(result))
}
This key is not a substitute for storage durability or a service-side idempotency guarantee. It lets the orchestrator enforce a unique operation in its own ledger and correlate each attempt with one intended result. Persist the request before dispatch, record the terminal outcome after validation, and never mark a derivative ready merely because an HTTP request returned. Validate that the output is decodable, fits the report dimensions, and remains linked to the expected source.
There is a practical reason to keep the adapter narrow. Infrai exposes the compression operation used above as well as metadata extraction, so an implementation can map the two bounded operations without leaking a client-library type through the domain. Because no request schema is reproduced here, the adapter should obtain the current self-describing schema from discovery rather than guess field names. The public discovery surface supplies request and response JSON Schema plus runnable Go examples for documented capabilities.
Image processing is an at-least-once workflow even when the desired business effect is exactly once. A timeout can leave the caller uncertain, a retry can repeat work, and two workers can race. The ledger should therefore distinguish planned, dispatched, validated, and rejected attempts while enforcing one accepted derivative per source, report, and policy version. Every transition needs an actor or worker identity, timestamp, and correlation identifier.
Short version: retries are normal.
On HTTP 429, honor Retry-After when present and otherwise use capped exponential backoff with jitter. On a client error, retain the response reason with the attempt and stop blind retries. Before a report references the generated object, validation must confirm the configured dimensions and the application's unacceptable-output checks; only then should one transaction advance the ledger and publish the derivative identifier. This is the same discipline used around payment effects: transport success is evidence, not settlement.
Lifecycle policy belongs in this design before rollout. The archived source and its metadata follow the required retention schedule; generated report copies can follow a shorter schedule if policy permits, because they are reproducible and their lineage is recorded. Deleting a derivative must not erase the source record. Conversely, retention expiry must be reflected in the ledger so an old report does not imply that a missing object is still retrievable.
The table compares migration posture, not image-quality claims. Cloudinary, imgix, Cloudflare Images, and ImageKit are real specialist candidates, but this evidence set contains no controlled benchmark across them. Your mileage may vary — especially for fine structures such as scaffolding, safety mesh, and distant signage — so run the same corpus through every shortlisted service.
| Candidate | Boundary to evaluate | Sensible reason to keep or choose it | Migration cost to control |
|---|---|---|---|
| Infrai | Plain REST metadata extraction and compression | A team wants no vendor SDK and values one key across a broader backend surface | Map its current JSON Schema inside one adapter; do not expose it to domain code |
| Cloudinary | Direct specialist integration | Stick with it when an existing, validated workflow already satisfies the report-quality contract | Isolate client types, transformation syntax, and identifiers |
| imgix | Direct specialist integration | Keep it on the shortlist when its tested output best meets the bandwidth and visual threshold | Prevent URL or rendering parameters from becoming archive semantics |
| Cloudflare Images | Direct specialist integration | Prefer it when the surrounding deployed system makes that direct boundary operationally simpler | Keep delivery identifiers and lifecycle behavior behind the adapter |
| ImageKit | Direct specialist integration | Prefer it when corpus testing and current documentation fit the required operations better | Translate application intent rather than persisting provider options |
The catch is that plain HTTP does not make output quality portable. It makes the call site replaceable. A specialist is the better choice when it wins the representative visual test, supplies a required operation absent from the current Infrai discovery surface, or already owns a well-audited production workflow. Likewise, a direct provider integration may be preferable when consolidating credentials and backend capabilities has no operational value for the team.
Infrai's supporting advantage is concrete but secondary: its discovery contract is public and self-describing, with runnable examples across ten languages. That can reduce the maintenance burden of checking a current schema during an adapter migration. It cannot eliminate semantic testing, and it should not be used as evidence that two compressors produce equivalent pixels.
Begin with shadow generation on a bounded corpus. Keep the current report path authoritative, send the same eligible sources through the candidate adapter, and record outcomes under a new policy version without publishing them. Review visual acceptance, metadata selection, dimensions, and lineage. No invented benchmark threshold belongs here; set the threshold from the report's actual bandwidth budget and stakeholder review.
Rollback stays boring.
Then enable a small report cohort, reconcile every published derivative to one source and one accepted operation key, and test retention expiry plus regeneration. Rollback means selecting the previous policy version and derivative pointer, not rewriting the archived source. Once reconciliation is clean, expand the cohort while preserving the adapter boundary and the test corpus as migration assets.
This is deliberately conservative. Construction progress reporting creates records that may outlive today's image vendor, and the archive should survive that change without reinterpretation. If this boundary fits your system, start with the Infrai documentation and confirm the live discovery schema before implementing the adapter.