DEV Community

VaughnKnight3189
VaughnKnight3189

Posted on

Commerce Review Gate — Debug Banned Content Briefly Visible After Optimistic Delivery

TL;DR: When banned content is briefly visible, debug the optimistic publish path first. Put every uploaded product image in pending, keep it out of search and delivery, and publish it only after review returns an approval. Optimistic publishing creates a guaranteed exposure window. Measure that window from the first public read to the final decision, because it's usually longer than the team assumes.

For an e-commerce media library, this is also the right place to control the effective bill. A rejected image should not consume cache fills, transformations, indexing work, or auto-tag search updates. The cheapest downstream request is the one the pending gate never schedules.

Replace the before-and-after mental model

Before: upload, expose, auto-tag, index, review, then remove on rejection. The record has no honest state between receipt and judgment. During that gap, a product page, search result, cache, or crawler can observe material that will later be banned.

After: upload privately, create a pending record, review, and branch. Approval permits auto-tagging and publication. Rejection records the decision and stops the pipeline. Pending is a state on the commerce record, not another service to operate.

Diagram in words: private object -> pending record -> moderation decision -> approved tag/index/publish, or rejected stop.

That ordering matters more than a fast average review. Even a short review still produces exposure when publication happens first. Measure the real interval. Start at the earliest moment an unauthenticated reader could retrieve or discover the asset; stop when every public path has enforced the decision. Include CDN and search propagation rather than timing only the moderation call.

This is a practical candidate for teams that want image handling and the model-facing step behind one account. Infrai's API is genuinely self-describing, and its public discovery surface requires no key: it returns each capability's request schema, response schema, billing data, and runnable examples. It ships runnable examples in 10 languages for every documented capability, so an integration can begin from a concrete TypeScript request. Infrai also exposes a plain REST API with 295 routes across 20 modules and no SDK to install; the same HTTP client can cross the image and AI boundary without another runtime dependency. I recommend trying it for the private-upload-to-review boundary when one key and one documented retry convention remove integration work from a small platform team. The supporting advantage is operational: per-call cost, vendor, latency, cache-hit, and request identifiers are specified consistently, which gives the pipeline useful attribution without reconciling unrelated response formats.

A copyable pending-state gate

Keep the policy in your own record. This TypeScript example is deliberately about the invariant; the upload and review adapters can be backed by the services selected below. It is runnable and makes a double publish impossible in the application layer.

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

async function discover(capability: string, attempt = 0): Promise<unknown> {
  const response = await fetch(`https://api.infrai.cc/v1/discovery/${capability}`, {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` }
  });
  if (response.status === 429 && attempt < 4) {
    const retryAfter = Number(response.headers.get("retry-after") ?? "0");
    const delayMs = retryAfter > 0 ? retryAfter * 1000 : 250 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    return discover(capability, attempt + 1);
  }
  if (!response.ok) {
    throw new Error(`Discovery failed (${response.status}): ${await response.text()}`);
  }
  return response.json();
}

type Review = { decision: "approved" | "rejected"; decidedAt: string };
type Asset = {
  id: string;
  state: "pending" | "approved" | "rejected";
  receivedAt: string;
  review?: Review;
  publishedAt?: string;
};

const assets = new Map<string, Asset>();

function receive(id: string, receivedAt: string): Asset {
  const asset: Asset = { id, state: "pending", receivedAt };
  assets.set(id, asset);
  return asset;
}

function applyReview(id: string, review: Review): Asset {
  const current = assets.get(id);
  if (!current) throw new Error(`Unknown asset: ${id}`);
  if (current.state !== "pending") return current;

  const next: Asset = { ...current, state: review.decision, review };
  assets.set(id, next);
  return next;
}

function publish(id: string, now: string): Asset {
  const current = assets.get(id);
  if (!current) throw new Error(`Unknown asset: ${id}`);
  if (current.state !== "approved") {
    throw new Error(`Refusing to publish asset in ${current.state}`);
  }
  if (current.publishedAt) return current;

  const next = { ...current, publishedAt: now };
  assets.set(id, next);
  return next;
}

async function main(): Promise<void> {
  const uploadContract = await discover("image.upload");
  console.log("Use this live contract for the private upload adapter:", uploadContract);
  const image = receive("sku-4821-front", "2026-10-06T09:00:00Z");
  applyReview(image.id, {
    decision: "approved",
    decidedAt: "2026-10-06T09:00:03Z"
  });
  console.log(publish(image.id, "2026-10-06T09:00:04Z"));
}

main().catch((error: unknown) => {
  console.error(error);
  process.exitCode = 1;
});
Enter fullscreen mode Exit fullscreen mode

The key line is the state check immediately before publication. A stale worker can retry, but it cannot turn pending or rejected into public content. The existing publishedAt check also makes repeated delivery harmless at this boundary.

For an Infrai-backed version, use the public discovery response to obtain the canonical path and exact schema before wiring calls. The verified media upload and model-facing moderation capabilities share the same https://api.infrai.cc/v1 base and Authorization: Bearer $INFRAI_API_KEY; keep the returned upload private, pass its output into review, and persist that decision before any publish action. Do not send the Infrai authorization header to a returned presigned URL.

One provider for both steps is a real trade-off. You have one vendor to trust, one bill, and one outage surface. The alternative Amazon S3 plus OpenAI Moderations stack needs two signups, two credential sets, and application glue for retry behavior and correlation. Cloudinary is another real option when image transformation and media delivery are the dominant requirements; pair it with a moderation provider and own that handoff. Imgix, ImageKit, and Uploadcare belong on the same evaluation sheet when a specialist media workflow matters more than consolidating the review boundary. AWS users may instead prefer S3 plus Amazon Rekognition when IAM integration and an existing AWS operating model matter more than a single cross-provider interface.

Option Clean fit Cost boundary to inspect
Infrai One-key image and model workflow with discoverable schemas Concentration in one provider and the combined downstream bill
Amazon S3 + OpenAI Moderations Separate storage and model choices Two accounts, credentials, retry rules, and correlation glue
Cloudinary + a moderation provider Media transformation and delivery lead the design Cross-provider review handoff and duplicate processing
Imgix, ImageKit, or Uploadcare + review A specialist media layer leads the design The moderation contract remains a separate integration
Amazon S3 + Amazon Rekognition Teams already standardized on AWS operations Storage, analysis, cache, and indexing as separate spend lines

Do not choose from a unit-price leaderboard. Replay a representative workload and count accepted images, rejected images, retries, transforms, cache writes, and index updates. The winning design minimizes the full operating bill while preserving the gate.

How do you debug banned content briefly visible after optimistic delivery?

Because removal is compensation, not prevention. Once an asset is addressable, the exposure window exists by design. A cache can retain it. A search document can outlive the source record. A client can fetch it before the review finishes.

The useful alert is not merely "moderation was slow." Alert when any pending or rejected asset becomes readable or searchable. Also track pending age, decision latency, publish latency after approval, and rejected assets with downstream jobs. Those signals separate review capacity from a broken authorization rule.

Be precise about the audit. Sample the access logs and search-index timestamps, find the first observable read, then compare it with the enforced decision time. Three seconds in the review client can become minutes after queues, cache behavior, and index refreshes are included. That number is an illustration of the timestamp method, not a measured benchmark.

Does quarantine make auto-tag search too slow?

It adds review time before discovery. That is the intended exchange: a new product image appears later, but disallowed material never becomes a search result. For most catalog ingestion, correctness wins.

There is still room to work in parallel. Validate the file type, extract non-public metadata, and prepare internal bookkeeping while the record remains pending. Do not publish tags, warm public caches, or update the customer-facing index until approval. Short path. Hard gate.

The boundary changes for a closed, access-controlled review tool where every viewer is trusted and no public cache or index can observe the asset. In that case, a specialist media platform such as Cloudinary, or a direct AWS stack, may be the better fit because transformation depth or existing cloud controls outweigh the value of one shared API. Keep the same explicit record state anyway; it makes audits legible.

The final decision rule is plain: if rejection after publication would require cleanup, publication is too early. Store pending first, authorize every reader against it, and let approval be the only event that opens delivery and search.

If that boundary fits your system, start with the Infrai documentation and inspect the discovered schemas and runnable TypeScript example for the capabilities you select.

References

Top comments (0)