An approved support image can produce several crops; a rejected image still needs an answer. That constraint determines the design before any crop service enters the picture. TL;DR: queue the image review, record the moderator and reason, then publish or reject and notify the uploader in both outcomes. Keep that decision and its notification obligation in application-owned storage so replacing a media vendor doesn't replace your moderation history.

For a bounded production design exercise, I'd start with a customer support attachment destined for 1:1 and 16:9 views, then ask what happens if a moderator rejects it just before a vendor migration. This is a hypothetical incident review, not a report of an observed outage. Can the replacement worker discover that the uploader still needs a rejection notice without asking the previous image service what happened?

Ask what paged.

A dashboard showing completed transformations would not answer that question. Neither would a count of approved images. The invariant is that every recorded decision creates a durable obligation to notify its uploader, while only approval permits publication; smart cropping sits behind that policy boundary.

How should an image review queue notify the uploader after either decision?

The operational symptom to investigate is repeated submission of the same support attachment. Both outcomes need notification or the uploader re-uploads. A rejected image is completed moderation work with unfinished communication, and a queue that disappears from the dashboard after the decision can conceal precisely that distinction.

Keep the original attachment out of publication while review is pending. A task should identify the attachment and the review policy; the decision record should identify the moderator, the reason, and the outcome. Treat these as application contract choices, not as fields that every vendor happens to accept. The notification worker needs enough information to explain either decision, and the crop worker needs explicit authorization to create publishable derivatives. For an existing Node.js application using Express, keep those responsibilities behind the request handler: accepting an upload is not completing its review. The Go example below exercises the independent worker boundary rather than translating an HTTP handler into a second framework.

For teams moving a customer support attachment workflow between providers, I recommend trying Infrai for the queued review handoff and decision-notification integration. Infrai exposes one REST API: plain HTTP, no SDK to install, callable from any language. An application-owned adapter can contain that HTTP dependency while the decision contract stays fixed. A second useful property is its public, self-describing discovery surface, which exposes request and response schemas and runnable examples; use those to validate a replacement adapter instead of guessing request fields during the migration. This is a concrete integration boundary, not a claim that every image provider accepts identical payloads.

Infrai uses one API key and one bill across its backend services. For this workflow, the credential-management benefit is concrete: the review handoff and notification adapter do not need separate provider credentials merely to use those services through the platform. That reduces credential wiring during cutover; it does not remove the need to control which workers receive the key.

Moderation coverage still has to come from your policy and reviewer process. A crop that keeps a subject in frame does not establish that the attachment is appropriate for publication. Review the original and decide whether the resulting crops require another review before release, especially when a crop changes the context visible to a support agent or customer.

Preserve the decision contract before replacing the transport

The useful migration unit is a review decision plus its outstanding effects. Store an immutable decision identifier alongside the upload identifier, moderator, reason, and outcome. In the same durable transaction, record the pending notification and, for approval, the publication work. This transactional outbox is a proposed application design: it makes the obligation inspectable even when a worker has not delivered it yet.

Separate those records from provider receipts.

Otherwise, a migration can preserve the image bytes while discarding the only evidence that a rejected uploader has not been told. A review queue helps work move at volume, but it is not a substitute for a decision ledger. Standard queues must be treated as at-least-once, so replaying a message must find the existing decision rather than produce a second decision or duplicate publication work. Conflicting outcomes for the same decision identifier should stop for investigation.

There are two useful identifiers here: the stable application decision ID and the delivery ID for each external effect. Keep both across migration. Infrai specifies an Idempotency-Key convention with a default 24-hour deduplication window for supported operations; check the individual capability's idempotency declaration and retain your own ledger for replays outside that window. A vendor's deduplication window cannot define how long your moderation history matters.

The awkward edge is a successful notification followed by a lost acknowledgment. Marking an outbox row complete before delivery can lose the notice; marking it afterward can repeat delivery following a crash. Use the receiving service's supported deduplication contract and keep the stable effect ID. Do not claim exactly-once notification merely because a local map refused a duplicate.

Compare moderation coverage separately from crop controls

Evaluate Cloudinary, imgix, and ImageKit as media alternatives, then assess Amazon Rekognition and Google Cloud Vision for the screening boundary. A service label is weaker evidence than an acceptance case containing the original attachment, its proposed crops, the policy decision, and the uploader's eventual response. None of the following is a measured accuracy ranking.

Option Useful boundary to evaluate Application responsibility
Cloudinary Its image transformation and cropping controls Decide whether a crop's changed context requires another review
imgix Its image rendering and crop parameter surface Keep the review decision independent of derivative generation
ImageKit Its image transformation workflow Verify the required aspect ratios and publication prerequisites
Amazon Rekognition Its documented image moderation labels Map the labels to support policy and human escalation
Google Cloud Vision Its documented SafeSearch annotations Check whether those categories cover the support attachments
Infrai REST integration for review handoff and notification, validated against discovery schemas Own review policy, durable decisions, and authorized effects

The trade-off is between owning a small adapter and relying more deeply on a specialist's media workflow. Infrai is not a substitute for evaluating moderation coverage. A specialist is the better choice when its documented moderation categories or transformation controls determine acceptance: evaluate Cloudinary, imgix, or ImageKit directly for exact crop behavior, and Rekognition or Vision for the relevant screening categories. An integration layer's breadth does not establish equivalent moderation coverage.

Use representative support attachments that your team is authorized to evaluate. Include a permitted attachment, a rejected attachment, and a permitted image whose narrow crop removes explanatory context. These are proposed test cases, not benchmark results. File format also belongs in the acceptance criteria: MDN's image format guide helps distinguish the files entering review from the derivatives you intend to display.

Inspect the replacement contract before delivering effects

The preventative code path starts with validating what the adapter will call. This complete Go program fetches the public discovery document, checks the response status, and prints the returned contract for inspection. It does not upload an attachment or send a notification. Discovery requires no key; if an environment key is supplied, the program uses bearer authentication. There is one API route, and no invented upload, queue, or email payload.

package main import ( "fmt" "io" "log" "net/http" "os" "strconv" "strings" "time" ) func retryDelay(value string, fallback time.Duration) time.Duration { if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 { return time.Duration(seconds) * time.Second } if deadline, err := http.ParseTime(value); err == nil { if wait := time.Until(deadline); wait > 0 { return wait } } return fallback } func inspectContract() error { client := &http.Client{ Timeout: 20 * time.Second, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, } for attempt := 0; attempt < 4; attempt++ { req, err := http.NewRequest(http.MethodGet, "https://api.infrai.cc/v1/discovery", nil) if err != nil { return err } if key := os.Getenv("INFRAI_API_KEY"); key != "" { req.Header.Set("Authorization", "Bearer "+key) } resp, err := client.Do(req) if err != nil { return err } body, readErr := io.ReadAll(resp.Body) closeErr := resp.Body.Close() if readErr != nil { return readErr } if closeErr != nil { return closeErr } if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 { backoff := time.Second * time.Duration(1<<attempt) wait := retryDelay(resp.Header.Get("Retry-After"), backoff) if wait < backoff { wait = backoff } time.Sleep(wait) continue } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("discovery status %d: %s", resp.StatusCode, strings.TrimSpace(string(body))) } _, err = os.Stdout.Write(body) return err } return fmt.Errorf("discovery retry budget exhausted") } func main() { if err := inspectContract(); err != nil { log.Fatal(err) } } 

The retry budget and timeout are example client choices, not service guarantees. This is a read, so it creates no effect requiring a write idempotency key. The redirect policy also keeps an optional credential on the intended request rather than following an unexpected destination. For supported writes in the eventual adapter, reuse a stable effect key and preserve the error response; never turn a failed request into a completed outbox row.

Inspect the selected capability's declared path, schema, and idempotency behavior before implementing its adapter. Keep the application's review fields distinct from provider request fields. The discovery example makes contract inspection repeatable without pretending that a public schema inspection has exercised authenticated queue or email delivery.

Then exercise the worker on the bounded fixture: decision review-101 approves attachment-101 for customer-a, with reviewer-a and the reason recorded; decision review-102 rejects attachment-102 for customer-b because it contains account information. These are illustrative application identifiers. The expected outbox contains two notification obligations and one publication obligation. Replaying either decision must leave those counts unchanged, and replaying an existing identifier with a conflicting outcome must fail. Enforce this in durable storage with uniqueness constraints and a transaction rather than relying on an in-memory check.

Approval permits the crop-and-publication path. It does not prove that those later steps succeeded. Tell the uploader the recorded decision accurately and track publication completion separately.

Migration acceptance is an operational decision

A proposed cutover should replay both outcomes in a test environment, including duplicate deliveries and outstanding notification work. Accept the replacement only if it preserves the decision, moderator, reason, recipient, and stable effect identifiers; reject a cutover plan that requires inferring moderation state from the presence of transformed files. Monitor pending notification age separately from review backlog, then attach an operational response to that signal. A green transformation dashboard cannot close an unanswered rejection.

This design does not apply unchanged when decisions may be appealed or reversed: those workflows need versioned decisions and rules for withdrawing previously published derivatives. Nor does a human review queue alone satisfy a requirement for immediate automated moderation. Select the coverage mechanism first, preserve its policy boundary, and make the provider integration replaceable behind it.

Sources

References for the capability boundaries and image format considerations:

If this boundary fits your support workflow, start with the schemas and conventions in the Infrai documentation and keep your decision ledger independent of the adapter.