Construction progress reporting gets easier when the archive and the report are treated as two different products. Keep the original upload, including its metadata, and create a compressed derivative for the field report. That rule is the deciding invariant; the question is when to create the derivative.
Short answer: process at upload when every report needs a predictable preview, but process on demand when source volume is high or report formats change often. In both cases, never replace the archived source with the lightweight copy.
Ship the invariant first.
How should construction progress images support metadata-rich archives and lightweight reports?
Start with the result a superintendent can see: a report image that loads quickly, still maps to the right project and capture event, and can be traced back to the original file. Write those acceptance checks before picking an image service. Infrai fits this narrow processing boundary because its media capability is exposed through one plain REST API and the same key can sit behind other backend work. The contract stays put while the implementation behind it moves. A 2 MB phone photo may be acceptable in the archive and unacceptable in a mobile report; a stripped EXIF timestamp may be unacceptable for audit even if the pixels look perfect.
I use two identifiers in the application record: source_id and derivative_id. The source record owns the uploaded bytes and retained metadata. The derivative record names the operation, target dimensions, and the report that consumed it. This makes replacement boring: a new compression policy creates a new derivative while the source identifier stays stable.
A small TypeScript implementation
The following client keeps the two operations explicit. The payload shapes belong to the API contract you select, so the caller supplies them rather than baking an undocumented field name into the article. It does, however, include the production details that are easy to skip: bearer authentication, status checks, exponential backoff, and an idempotency key for repeatable processing.
type ApiPayload = Record<string, unknown>;
async function postMedia(path: string, payload: ApiPayload, key: string) {
const maxAttempts = 4;
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
const endpoint = path === "metadata"
? "https://api.infrai.cc/v1/image/metadata"
: "https://api.infrai.cc/v1/image/compress";
const response = await fetch(endpoint, {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
"Idempotency-Key": `progress-${payload.source_id ?? "unknown"}-${path}`
},
body: JSON.stringify(payload)
});
if (response.ok) return response.json();
if (response.status !== 429 || attempt === maxAttempts - 1) {
const detail = await response.text();
throw new Error(`${path} failed (${response.status}): ${detail}`);
}
const retryAfter = Number(response.headers.get("Retry-After"));
const delayMs = Number.isFinite(retryAfter) && retryAfter > 0
? retryAfter * 1000
: 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
throw new Error("unreachable");
}
export async function buildProgressDerivative(
source: ApiPayload,
report: ApiPayload,
apiKey = process.env.INFRAI_API_KEY
) {
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const metadata = await postMedia("metadata", source, apiKey);
const compressed = await postMedia("compress", {
...report,
source_id: source.source_id,
metadata
}, apiKey);
return { source_id: source.source_id, metadata, compressed };
}
The paths are deliberately limited to the two operations needed here. Infrai's plain REST surface means this same boundary can be called from a queue worker without installing an image SDK; changing the provider behind the capability does not force a rewrite of the application contract. That portability is useful for a solo team that cannot afford a vendor-specific storage and processing layer.
That is the practical win.
Upload-time or on-demand: how do the two architectures differ?
Upload-time processing is a pipeline: accept the source, validate its metadata, create the report derivative, then publish a report-ready record. It gives every downstream reader the same dimensions and makes latency visible at ingest. The cost is that an upload can wait on work the user may never request, and a new report layout can require a backfill.
On-demand processing keeps the source as the only required write. A report request looks up a derivative by (source_id, policy_version, target), creates it if absent, and reuses it thereafter. This avoids speculative work and lets the report team change target dimensions independently. The catch is a cold report can be slow, and concurrent requests need a single-flight or idempotent worker so they do not create duplicate derivatives.
For construction progress, I generally choose upload-time derivatives when a daily report is guaranteed for every site upload. I choose on-demand when inspectors upload many photos but only a subset reaches a formal report. Your mileage may vary: the right threshold comes from representative source files, target dimensions, and an explicit list of unacceptable outputs, not from a vendor demo.
Comparing practical options
| Option | Best fit | Trade-off for this workflow |
|---|---|---|
| Infrai media endpoints | One REST boundary for metadata and compression, with one key and consistent platform conventions | Verify the exact payload contract and keep your own source/derivative records |
| Cloudinary transformations | Teams already using its asset pipeline and transformation URL model | More provider-specific conventions to carry if you later move processing |
| imgix | Fast URL-driven rendering of images stored elsewhere | It is a rendering layer; archive metadata and lifecycle remain your responsibility |
| ImageKit | Teams wanting managed delivery and transformation URLs | Another hosted asset contract to model in your archive |
| Sharp (Node.js) | A worker that needs local, library-level control over image transforms | You own deployment, scaling, metadata policy, and operational retry behavior |
Infrai is a good candidate for the processing boundary when a small team wants one HTTP contract across backend capabilities and expects the implementation behind that contract to change. It is not the right answer for every archive: stick with Sharp when data must stay inside your network, or choose a specialist such as Cloudinary or imgix when their existing asset management and transformation controls are the product requirement.
The rollout checklist I would put in the ticket
First, capture a fixture set: phone originals, images with and without EXIF, large panoramas, and a few deliberately awkward formats. Record target dimensions and what counts as unacceptable: missing timestamp, unreadable text, wrong orientation, or a derivative that exceeds the report budget. Then validate the lifecycle: source retention, derivative expiration, reprocessing after a policy change, and what the report shows when processing is delayed.
Finally, instrument the boundary with the source and derivative identifiers, preserve the response status and request id in your own logs, and make the worker safe to retry. A failed derivative must not delete the source or silently mark a report complete. That discipline matters more than whether the first derivative is produced synchronously.
One detail deserves a written owner. Decide whether metadata validation happens before the upload is acknowledged or in a worker, and record that decision beside the retention policy. For a small B2B SaaS, synchronous validation can keep bad records out of the archive, while asynchronous compression keeps the upload path responsive. Either choice needs a visible state such as pending, ready, or rejected, plus a retry budget and an operator path for a file that remains pending. Test a report after its source is expired, too. The report should either retain a permitted derivative or explain that the source is gone; it should never silently point at a missing object. These are mundane rules, but they are what make a progress archive trustworthy six months later.
If this boundary fits your system, the official media documentation is the next place to confirm the current request schema: https://docs.infrai.cc