Short answer: For marketplace listings in a B2B SaaS product, put every public image variant behind one moderation decision. Use versioned named transformations for three approved display roles. Keep inline operation lists for experiments that cannot publish until reviewed. The choice is about which bytes can reach a buyer after a seller replaces a photo.
| Choice | Moderation coverage | Change surface |
|---|---|---|
| Named roles | One allowlist of publishable roles | A recipe edit affects every caller |
| Inline lists | Each distinct list needs policy validation | A caller can alter one rendition |
Pick named roles when the same image appears in thumbnails, search results, and detail views. Pick inline lists when renditions genuinely differ and the delivery gateway validates every operation. Neither option makes an unreviewed upload safe.
The gate matters more.
What does moderation need to cover?
Moderate the source before issuing a public delivery reference. Bind the decision to the exact source revision. If a seller replaces a photo, approval of the old bytes must not authorize derivatives of the new bytes. Keep originals private and public renditions behind the same access check. A deny decision must stop all roles, including cached outputs; plan cache invalidation and authorization together.
A named role limits which outputs exist, but does not substitute for content review. Cropping can hide context that an original-image check relied on. Inline lists can introduce a new crop without a reviewer noticing. Check the input revision and review transformations that substantially change visible content.
Count approved source revisions whose every reachable public variant is governed by the same decision, divided by source revisions with any reachable public variant. Separately count denied revisions that remain reachable. The target for that second count is zero. An upload-job success rate cannot detect a cached derivative leaking after a reversal.
Consider a replacement arriving between approval and delivery: the approval record still says yes, the image identifier is unchanged, and an image service eagerly produces all three renditions from the new bytes. If the gate checks only the identifier, the new search thumbnail can become public before anyone reviews the replacement. Comparing immutable revisions at the final serving boundary closes that gap, while a cache keyed by identifier alone can reopen it. Test both paths with the same replacement fixture; a successful upload response does not prove that viewers see only approved bytes.
How do you make the boundary executable?
The delivery service, not each UI component, should enforce the rule. This TypeScript example constructs a request only for reviewed roles and a revision-specific approval. Its dimensions are illustrative policy inputs, not measured optimums.
type Role = "thumbnail" | "search" | "detail";
type Decision = { revision: string; approved: boolean };
type Image = { id: string; revision: string };
const roles: Record<Role, { width: number; height: number }> = {
thumbnail: { width: 160, height: 160 },
search: { width: 640, height: 480 },
detail: { width: 1280, height: 960 },
};
function deliveryRequest(image: Image, decision: Decision | undefined, role: Role) {
if (!decision?.approved || decision.revision !== image.revision) {
throw new Error("Image revision is not approved");
}
return { imageId: image.id, revision: image.revision, recipe: "listing-v1", ...roles[role] };
}
const image = { id: "listing-42-photo-1", revision: "rev-2" };
console.log(deliveryRequest(image, { revision: "rev-2", approved: true }, "search"));
The service must verify approval and revision again when resolving that request. It must also reject direct requests for arbitrary operations. Otherwise the typed frontend allowlist is theater. Exercise missing approval, denied approval, stale revision, and replacement uploads while an old rendition is cached. Measure upload-to-first-approved-view and reversal-to-last-reachable-derivative independently. Speed is not coverage.
No shortcut there.
Format selection is a separate concern. Image formats differ in compression and browser support; negotiate a suitable output and check the actual content type and bytes in tests. Compare real listing photos at each role's dimensions for legibility, crop, and payload, rather than trusting a single sample.
When do inline lists age better?
If each tenant has a different listing layout, a fixed role catalog may grow into dozens of nearly identical entries. Inline operations can express those differences close to the caller. The cost is policy in more places. Parse operations into a typed set, cap dimensions, reject unknown operations, and log the normalized list with the source revision and approval decision. Do not inspect URL strings as a substitute for validation. Benchmark distinct approved lists and review time for a new layout, not just output byte size.
Named roles also have a trap. Changing listing-v1 in place lets cached and newly generated renditions disagree under one name. Add listing-v2, inspect moderation-sensitive crops and output size, deploy its resolver, then migrate callers. Retire old renditions according to the source image's policy. That version housekeeping buys an inspectable rollout.
The limitation of named roles is catalog growth: they are a poor fit when each tenant needs distinct framing and reviewers can approve operation parameters directly. The trade-off with inline lists is more validation code and a larger set of public outputs to audit. Neither choice should bypass source-revision checks.
For a small SDK or CLI, one role argument has less glue and fewer ways to bypass review. Inline lists offer control but push configuration into every caller. Track rejected requests by reason, variant count per source revision, and reversal lag. Those numbers reveal whether the abstraction still matches the system.
Further reading
- MDN, Image file type and format guide.
- OWASP, File Upload Cheat Sheet.
- MDN, HTTP caching.
Top comments (0)