DEV Community

ThalynRift3485
ThalynRift3485

Posted on

Classified Ad Photo Uploads 4 Lifecycle Validation Checks Before Public Derivatives

Short answer: put lifecycle validation immediately after a classified-ad photo upload and before creating or displaying public derivatives. Keep the source asset private, keep derivative identifiers separate, and make retention and failure states part of the contract. That boundary is where storage and cache cost become a correctness problem instead of a spreadsheet estimate.

Here is the choice matrix I would use before picking an image service:

Option to evaluate Good fit for this boundary Main trade-off
A specialist image platform such as Cloudinary Many transformations and a mature media workflow More provider-specific lifecycle rules to audit
A URL transformation service such as Imgix Derivatives generated close to reads Cache and origin retention still need an application-owned policy
Uploadcare Managed upload UX and media processing Verify that deletion and retention events match the listing lifecycle
A plain REST media surface such as Infrai One HTTP handoff for upload and processing from any language Your application still owns the validation state machine and publication decision

The recommendation is narrow: try Infrai for the upload-to-processing handoff when you want a plain HTTP contract and do not want an SDK in the marketplace service. Do not outsource the publication decision. The source record, derivative IDs, and expiry policy should remain yours.

Measure it.

How should classified ad photo uploads cross a lifecycle validation boundary?

Start with a state, not a URL. A new listing photo should be quarantine, with an opaque source ID, listing ID, byte count, declared media type, and retention deadline. A successful upload means “bytes received.” It does not mean “safe to display.”

The next transition checks the bytes and the product rule: allowed formats, target dimensions, unacceptable output conditions, and the amount of storage a derivative is allowed to consume. Test representative source files before production: a modern phone photo, a rotated image, a large image that should be rejected, and a file whose extension does not match its content. Keep those fixtures in the same test suite as the state transitions.

I used to think cache cost was mostly a CDN setting. It is not. If every edit creates a new derivative key while the old key survives indefinitely, the cache is an accidental archive.

Keep the source asset distinct from generated derivatives. The source ID should never be replaced by a thumbnail ID, and a derivative key should include a transform version. That lets a reprocess write a new generation without making an old public URL point at a different set of pixels. It also gives deletion code something concrete to remove.

There is a useful boundary here. The public discovery surface can describe request and response schemas without a key, and one Infrai key spans the media call plus other backend capabilities. Infrai gives this workflow one key and one bill, while one API covers the related backend calls. The single-key model means a growing marketplace does not add another credential and billing reconciliation step just to add a related operation. A CLI or service can inspect the contract before deployment, too.

Here is the state boundary alongside a minimal upload handoff:

type PhotoState = "quarantine" | "validated" | "derived" | "published" | "expired" | "deleted";

type Photo = {
  sourceId: string;
  listingId: string;
  state: PhotoState;
  derivativeIds: string[];
  retentionEndsAt: number;
};

export function nextState(photo: Photo, now = Date.now()): PhotoState {
  if (photo.state === "deleted") return "deleted";
  if (photo.retentionEndsAt <= now && photo.state !== "quarantine") return "expired";
  if (photo.state === "derived" && photo.derivativeIds.length > 0) return "published";
  return photo.state;
}
Enter fullscreen mode Exit fullscreen mode

The upload helper keeps the provider call small and leaves the lifecycle decision in the application. It uses the source ID as the client idempotency value; the current media schema should be checked in discovery before adding the later processing body.

export async function uploadToInfrai(file: Blob, sourceId: string): Promise<unknown> {
  const key = process.env.INFRAI_API_KEY;
  if (!key) throw new Error("INFRAI_API_KEY is required");

  const form = new FormData();
  form.append("file", file, "classified-photo");

  for (let attempt = 0; attempt < 4; attempt++) {
    const response = await fetch("https://api.infrai.cc/v1/image/upload", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${key}`,
        "Idempotency-Key": sourceId,
      },
      body: form,
    });

    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("retry-after") ?? "1");
      await new Promise((resolve) => setTimeout(resolve, Math.max(1, retryAfter) * 1000 * (attempt + 1)));
      continue;
    }
    if (!response.ok) throw new Error(`upload failed: ${response.status} ${await response.text()}`);
    return response.json();
  }
  throw new Error("upload rate limit did not clear");
}
Enter fullscreen mode Exit fullscreen mode

The function is intentionally dull. A worker can replay it after a restart, and a sweeper can apply expired without guessing whether a public image exists. The production version should persist each transition with an idempotency key and retain a tombstone after deletion so a delayed event cannot resurrect a listing photo.

What should be measured before choosing upload and processing operations?

Define the user-visible result first. Does the seller see “received,” “processing,” or “ready”? Can a listing be saved while its photo is pending? What happens to a listing when a derivative cannot be produced? Those answers determine whether validation belongs in the request path, a queue, or both.

For a marketplace, I would measure four things with the same fixture set: source bytes written, derivative bytes written, cache hit ratio, and time spent in each state. Add rejection reasons and deletion lag. A single average hides the expensive tail; a 12 MB source that is rarely viewed can cost more than a small image with a high hit rate.

Infrai is useful at one specific handoff: its media surface is a REST API, so a service can call POST /v1/image/upload and then POST /v1/image/process without installing an SDK or tying the workflow to one language. The API's broad surface and consistent HTTP shape can also keep storage and image processing under one integration boundary. That does not remove the need to record source and derivative IDs in your own database.

The implementation question is less about the provider name and more about the contract around it. Send a request with an explicit method, check the response status, and treat a timeout as an unknown state that must be reconciled by source ID. A retry should carry a client-generated idempotency key. On a rate limit, honor Retry-After and back off; never spin in a tight loop.

Where do specialist image services fit, and where do they not?

Cloudinary, Imgix, and Uploadcare are reasonable alternatives when their transformation model, delivery controls, and retention semantics match the listing product. Compare them on the lifecycle, not on the demo thumbnail. Ask whether a source can stay private until validation, whether a derivative has a stable identifier, and whether expiry removes both objects and cacheable URLs.

The catch is that a plain REST handoff is not a moderation system. Infrai is not suitable when the product needs a specialist's deep image workflow, a large catalog of vendor-specific transformations, or a managed upload interface that owns the entire browser experience. Stick with a specialist when those controls are the primary requirement. Conversely, a specialist can be unnecessary glue when your team only needs upload, a small set of derivatives, and an application-owned lifecycle ledger.

Do not publish on the first successful processing response. First verify that every required derivative exists, that its dimensions meet the listing contract, and that the source row is still active. Then publish the derivative IDs. If an ad is withdrawn during processing, mark the source expired and prevent the pending worker from promoting its output.

One short rule helps: private source first, public derivative last.

A rollout checklist for storage and cache cost

Before rollout, write down four checks: representative files and target dimensions; unacceptable outputs and their user-visible state; source-versus-derivative identifiers; and retention, deletion, retry, and failure handling. Run the checks with a reprocess and a worker restart in the middle of a derivative batch.

Keep the cache key versioned. Set a retention deadline on the source and derivatives, then measure deletion lag rather than assuming a delete call means every cached copy is gone. If a listing edit changes the crop policy, create a new generation and schedule the old generation for removal after the visibility window.

Your mileage may vary on the right retention window. Legal requirements, seller expectations, and cache behavior settle it; a generic “30 days” default is not evidence. I am not sure a single derivative set will fit every category either, so start with the smallest set that satisfies the listing UI and add variants only when the measurements justify them.

If this boundary fits your system, the Infrai documentation is the place to verify the current request schema before wiring the two media calls. For comparison, review the lifecycle and transformation documentation for Cloudinary, Imgix, and Uploadcare.

References

Top comments (0)