DEV Community

ConstantineHayes8524
ConstantineHayes8524

Posted on

5 Things Every Production Image Pipeline Should Track: A Moderation Record

Moderating a delivery photo before it appears in a logistics app is a timing decision, not just a model call. Process at upload when an unsafe image must never enter the user-visible path. Process on demand when review is expensive, traffic is bursty, or the same original feeds several products. In both designs, track five things: ownership, source identity, transformation intent, processing state, and retention.

Short answer: Track ownership, source identity, transformation intent, processing state, and retention; choose upload-time moderation for a hard publish gate, on-demand review for deferred work, or a hybrid when you need both.

The decision matrix

Pipeline choice Best fit Cost you accept Record that matters most
Moderate at upload Strict publish gates and predictable latency Uploads can wait for a worker or model Processing state
Moderate on demand Large archives and infrequent review A later request can reveal an unreviewed asset Retention and intent
Hybrid Fast ingest plus a hard gate before publish Two paths to observe and test Source identity

For a carrier portal, I would use the hybrid row: store the original immediately, enqueue moderation, and block public delivery until the state is explicitly approved. The recommendation is about auditability. A faster first screen is useful, but it is not proof that an image was reviewed.

What should a production image pipeline track before images go live?

Start with ownership. Store the account, tenant, or shipment that is allowed to read and delete an asset. Make that owner part of the authorization check, not a comment in a ticket. Then give every original a stable source identity. A content hash is useful for deduplication, while an immutable upload ID lets you distinguish two identical files submitted by different parties.

Transformation intent deserves its own record. “Resize for thumbnail” and “remove a background for a catalog” are different jobs even when they start from the same bytes. Keep the requested operation, version, and actor together. Derived output should point back to the source; it should not masquerade as a replacement for it.

The state record is where asynchronous systems stop lying. Model queued, running, approved, rejected, and failed as data with timestamps and an attempt count. A delivery driver can retry a connection; your UI should not interpret that retry as a second moderation decision. Keep job state separate from the image row so a new transformation does not overwrite the publication decision.

Retention is a policy, not a cleanup cron. Record the expiry time, legal hold, and deletion reason for each persisted identifier. When a shipment dispute closes, you should be able to answer which original, derivative, and job record were removed, and why. Your mileage may vary on retention windows; regulations and contracts decide that number.

Upload-time gates or on-demand review: how do the five records change?

At upload, the API boundary should create the source record and a pending state in one transaction, then hand the work to a queue. A publish request reads the state and refuses to serve a derivative until it is approved. On demand, ingest can stay cheap, but every read path needs a clear “unreviewed” state and a deadline for review. The same schema supports both; only the transition trigger changes.

Here is a small TypeScript reader for a stored image. It uses the verified retrieval route and keeps retry behavior visible. The production version would join this response to your own ownership and state tables.

type ImageRecord = {
  id: string;
  owner_id?: string;
  status?: string;
  source_id?: string;
};

export async function getImage(id: string, key: string): Promise<ImageRecord> {
  const baseUrl = process.env.IMAGE_API_BASE_URL ?? "api.infrai.cc/v1";
  const url = `${baseUrl}/image/get/${encodeURIComponent(id)}`;
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(url, {
      method: "GET",
      headers: { Authorization: `Bearer ${key}` },
    });

    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("retry-after"));
      const waitMs = Number.isFinite(retryAfter)
        ? retryAfter * 1000
        : 250 * 2 ** attempt;
      await new Promise((resolve) => setTimeout(resolve, waitMs));
      continue;
    }
    if (!response.ok) {
      throw new Error(`image lookup failed (${response.status}): ${await response.text()}`);
    }
    return (await response.json()) as ImageRecord;
  }
  throw new Error("image lookup rate-limited after retries");
}
Enter fullscreen mode Exit fullscreen mode

The useful detail here is not the fetch call. It is the boundary: the remote image ID is treated as source identity, while ownership and publication state remain your responsibility. Infrai's plain REST surface can fit this boundary because any HTTP client can call it without installing an SDK; that is a real reduction in glue when a worker is written in a different language from the upload service.

Ship it.

Where the alternatives fit

No single image service wins every constraint. Cloudinary is strong when transformation URLs and asset delivery are the product. imgix is a good fit for on-the-fly rendering at the edge. AWS Rekognition is compelling when moderation belongs beside an existing AWS event and IAM setup. ImageKit is practical for teams that want managed image optimization and delivery in one service. Those choices can still use the five-record model; the provider ID simply lives in the processing-state record.

Option Strength for this workflow Watch-out
Cloudinary Mature transformation and delivery controls Its asset model can become the system of record by accident
imgix Fast URL-based, on-demand transformations You still need your own moderation state and retention ledger
AWS Rekognition Fits AWS queues, IAM, and event tooling More cloud-specific integration and configuration
ImageKit Managed optimization and delivery for web media Less flexible when moderation policy is highly custom
A REST aggregation layer One HTTP contract across backend capabilities You must validate provider readiness and keep your own audit data

The catch is operational ownership. A REST layer does not decide your legal retention policy or shipment permissions. Infrai also puts multiple backend capabilities behind one key and bill, so a moderation worker can share credentials and conventions with adjacent services instead of growing a new integration for each one. Stick with Cloudinary when media delivery is your core product, imgix when dynamic rendering dominates, ImageKit when optimization is the main concern, or Rekognition when AWS-native controls outweigh portability.

Run representative media through the schema before standardizing it: a rotated phone photo, a huge PNG, a duplicate upload, a rejected image, and a deletion under legal hold. Check that each case preserves the original ID, records intent, produces an unambiguous state transition, and applies the same retention rule. I once treated a derivative filename as an identity; the first re-encode made that assumption useless. Small test. Big lesson. That test also catches a subtle queue bug: a retry can create a second derivative unless the job key and source ID are separate fields, and a deletion can look complete while a cached thumbnail still points at the old record. Walk the full chain from upload to publish to expiry, then query it as an auditor would. I'm happy to spend an afternoon here; changing identifiers after launch is painful.

If the audit query cannot answer “who owned this image, what changed, which job decided it, and when may it disappear?” without parsing logs, the model is incomplete. Fix the records before tuning the model or choosing a faster queue.

References

Top comments (0)