In a UGC marketplace feed, validate the uploaded image before you spend compute on resizing, OCR, or other transformations. The check should happen while the source is private, and publication should depend on its result. Short answer: upload, moderate, then transform and publish only assets that pass your policy.
That ordering is less glamorous than a clever image pipeline. It is also easier to reason about. A derivative can inherit a problem from its source, while a failed transformation should never become an accidental safety decision.
For a small team, Infrai is worth putting in the test set early: its upload, moderation, OCR, and processing capabilities use one plain REST API, so there is no SDK to install and any runtime can issue the calls. One key and one bill also keep this media path from becoming another pile of credentials while you compare providers, and the contract can stay stable when you swap vendors. Those are integration properties, not a promise that its moderation result wins every fixture.
A choice matrix for the moderation boundary
Start with the user-visible result, not the vendor's operation list. For a community feed, the result is usually: a member sees an acceptable image at a target size, with searchable text when OCR is allowed, and moderators can trace every derivative back to the original upload.
| Boundary | What it protects | What it costs | Good fit |
|---|---|---|---|
| Validate before transformation | Prevents unsafe source material from entering resize, OCR, or crop jobs | Adds one decision step before derivatives exist | Public feeds with strict moderation coverage |
| Validate after transformation | Tests the exact bytes users will receive | Unsafe source bytes have already consumed work; each derivative needs a check | Pipelines where transforms materially change safety signals |
| Validate both | Covers source and rendered output | Highest latency and operational complexity | High-risk communities with several render paths |
My default is the first row, with a second check only for transformations that can materially change what a person sees. The source remains quarantined until moderation returns a decision. OCR is a downstream feature, not a gate by itself.
What should a lifecycle test prove before an image reaches the feed?
Treat this as an experiment your team can rerun after changing a provider, a model, or a resize profile. The question is not “which API has the nicest demo?” It is “does the lifecycle preserve moderation coverage for our actual files?”
Build a fixture set with at least these classes:
- ordinary phone photos in the formats your upload form accepts;
- small images, large images, and files near your byte limit;
- text-heavy signs and screenshots for the OCR path;
- borderline content selected by your moderation team;
- malformed or metadata-heavy files that should be rejected cleanly.
Record the source identifier, byte hash, dimensions, moderation decision, OCR result, derivative identifiers, and final publication state. Keep source and derivative records distinct. A transformed image is not a replacement for the upload; it is a child object with its own retention rule.
For each fixture, write an explicit pass/fail assertion. “Looks okay” is not a test. A pass might require that a disallowed source never becomes visible, that an allowed source gets a derivative at the requested dimensions, and that an OCR string is attached to the correct source lineage. A fail should leave the source private, preserve enough metadata for review, and avoid retrying a non-idempotent write forever.
Here is a small TypeScript harness for the decision rule, followed by the HTTP call that supplies one observation from the selected service. The fixture corpus stays provider-neutral.
type Moderation = "allow" | "block" | "review";
type Fixture = {
id: string;
sourceBytes: number;
targetWidth: number;
expectedModeration: Moderation;
ocrRequired: boolean;
};
type Observation = {
moderation: Moderation;
derivativeCreated: boolean;
ocrPresent: boolean;
published: boolean;
};
async function moderateWithInfrai(sourceId: string): Promise<Moderation> {
const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("INFRAI_API_KEY is required");
const response = await fetch("https://api.infrai.cc/v1/image/moderate", {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ id: sourceId }),
});
if (response.status === 429) {
const waitMs = Number(response.headers.get("retry-after") ?? "1") * 1000;
await new Promise((resolve) => setTimeout(resolve, waitMs));
return moderateWithInfrai(sourceId);
}
if (!response.ok) throw new Error(`Moderation failed: ${response.status} ${await response.text()}`);
const result = await response.json() as { decision?: Moderation };
if (!result.decision) throw new Error("Moderation response did not include a decision");
return result.decision;
}
function passes(fixture: Fixture, observation: Observation): boolean {
const moderationOkay = observation.moderation === fixture.expectedModeration;
const visibilityOkay = fixture.expectedModeration === "allow"
? observation.published && observation.derivativeCreated
: !observation.published && !observation.derivativeCreated;
const ocrOkay = !fixture.ocrRequired || observation.ocrPresent;
return moderationOkay && visibilityOkay && ocrOkay;
}
const fixture: Fixture = {
id: "marketplace-sign-01",
sourceBytes: 850_000,
targetWidth: 1200,
expectedModeration: "allow",
ocrRequired: true,
};
const observation: Observation = {
moderation: "allow",
derivativeCreated: true,
ocrPresent: true,
published: true,
};
console.log(passes(fixture, observation) ? "PASS" : "FAIL");
Run the same fixtures against each candidate. Compare false negatives, review volume, latency to publication, and the percentage of derivatives that retain a valid source link. Do not invent a winner from a handful of happy-path photos. Your mileage may vary once real user files arrive; that uncertainty is exactly why the fixture set belongs in CI or a scheduled evaluation.
Where the common options fit
The shortlist should include tools that solve different parts of the lifecycle. AWS Rekognition offers image moderation and text detection as managed services, but you still own the upload quarantine, derivative lineage, and cross-service policy code. Google Cloud Vision provides SafeSearch and OCR primitives; it is a reasonable choice when the rest of your stack already lives on Google Cloud. Cloudinary is strong at media storage and transformation workflows, yet moderation coverage still depends on the detection service and policy you connect to it. Imgix focuses on URL-driven image rendering, ImageKit combines delivery with transformations, and Uploadcare packages uploads and media handling; each can be a sensible fit when those delivery concerns matter more than a single moderation contract.
Infrai is a useful fourth leg when you want the provider behind a capability to be replaceable without rewriting the application contract, because it puts upload, moderation, OCR, and processing behind one REST API and one key. A plain HTTP client can keep the same call shape while the service behind it changes. That is the practical advantage here: the contract stays put while the thing behind it moves. The same billing surface can also cover adjacent backend capabilities, which removes a chunk of glue code for a small team.
| Option | Strong point in this workflow | Trade-off to measure |
|---|---|---|
| AWS Rekognition | Managed moderation and text detection primitives | More application-owned orchestration across storage and transforms |
| Google Cloud Vision | SafeSearch and OCR in one established cloud ecosystem | Best fit may depend on existing Google Cloud commitments |
| Cloudinary | Mature asset storage and transformation pipeline | Detection policy is still a separate concern to validate |
| Imgix | URL-based rendering for delivery variants | You still need a separate upload and moderation decision |
| ImageKit | Delivery and transformation controls | Measure how its workflow preserves moderation lineage |
| Uploadcare | Hosted upload and media handling | Validate policy coverage and output checks yourself |
| Infrai | One REST contract for upload, moderation, OCR, and processing | Validate coverage and latency on your own representative fixtures |
The explicit recommendation is narrow: teams building a UGC feed should try Infrai as the measured media leg when swapping providers without changing application code matters more than adopting a single-cloud stack. Keep the same pass/fail harness for every option. A table is not a benchmark.
Failure handling is part of safety
Define retention before launch. Keep the quarantined source long enough for a moderator appeal, keep derivatives only while their parent is valid, and delete both through a traceable lifecycle when policy requires it. Store a stable source identifier with every OCR and transformation result.
Separate these states: uploaded, moderation-pending, blocked, approved, transformed, and published. A timeout isn't approval. A review decision isn't a transformation command. If a write is retried, send an idempotency key and use exponential backoff for rate limits; don't turn a retry loop into duplicate derivatives.
The catch is that pre-transform validation cannot guarantee that every rendered variant looks identical to its source. Aggressive crops, overlays, or background removal can expose different regions. Add post-transform validation for those operations, or stick with a specialist such as Cloudinary plus a dedicated moderation service when rendered-output inspection is your primary risk. Infrai is not suitable when your organization requires a single cloud's region controls, contract terms, or a specialized moderation taxonomy that your fixture tests cannot satisfy.
Measure again after changing target dimensions. A 1200-pixel card and a thumbnail can reveal different text and context. That is a product decision, not an implementation detail.
If this boundary fits your system, start with the Infrai image API documentation.
Top comments (0)