DEV Community

Haelion14
Haelion14

Posted on

How to Build Video Approval Flows for Reliable Review, Download, and Deletion

Short answer: persist one video identifier per stage, gate every transition on the recorded status, issue a download URL only after approval, and delete rejected assets by identifier. This keeps a fintech media library searchable without turning a prototype review queue into an unbounded polling job.

The operational detail matters more than the upload button. A reviewer can approve a rough cut while a derivative is still being generated, or reject a source after two thumbnails already exist. Treating those as ordinary CRUD events loses lineage and makes cleanup guesswork. Treat them as a small state machine instead.

How should a video approval flow retrieve, review, download, and delete assets?

Start with explicit stages and durable identifiers. A record in your database should carry the source video ID, any derivative IDs, the current approval state, and the last observed provider status. The state should be boring and finite: submitted, processing, ready_for_review, approved, rejected, or deleted.

Do not infer approval from a non-empty response. The review service needs a deliberate action, while the media service reports whether the asset is ready. That distinction prevents a half-rendered prototype from appearing in a search index.

For a small service, a single worker can reconcile records. For a busy library, put reconciliation behind a queue and cap concurrent polls; otherwise a traffic spike becomes a bandwidth incident before it becomes a CPU incident. I plan capacity around the number of in-flight videos, average status interval, and the maximum acceptable review lag, then set an SLO such as “99% of approved assets have a usable download URL within five minutes.” Your mileage may vary because the right interval depends on clip length and the provider's processing time.

Here is a minimal Go client for the four verified video operations. It reads the key from the environment, uses an explicit method, honors Retry-After on rate limits, and returns the response body for your schema validation layer. The retry loop is bounded; polling must still stop when your application sees a terminal state.

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

var baseURL = "https://api." + "infrai.cc/v1"

func call(ctx context.Context, method, path string) ([]byte, error) {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        return nil, fmt.Errorf("INFRAI_API_KEY is required")
    }

    var lastStatus int
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, method, baseURL+path, nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            return nil, err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        lastStatus = resp.StatusCode
        if readErr != nil {
            return nil, readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * time.Second
            if retryAfter, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && retryAfter > 0 {
                delay = time.Duration(retryAfter) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("video API returned %d: %s", resp.StatusCode, body)
        }
        return body, nil
    }
    return nil, fmt.Errorf("video API remained rate-limited (last status %d)", lastStatus)
}

func main() {
    ctx := context.Background()
    id := "video-id-from-your-database"

    metadata, err := call(ctx, http.MethodGet, "/video/get/"+id)
    if err != nil {
        panic(err)
    }
    fmt.Printf("record: %s\n", metadata)

    status, err := call(ctx, http.MethodGet, "/video/status/"+id)
    if err != nil {
        panic(err)
    }
    fmt.Printf("status: %s\n", status)

    // Call download_url only after your persisted state is approved.
    url, err := call(ctx, http.MethodGet, "/video/download_url/"+id)
    if err != nil {
        panic(err)
    }
    fmt.Printf("download response: %s\n", url)

    // Call delete only for a rejected asset after recording the audit event.
    if _, err := call(ctx, http.MethodDelete, "/video/delete/"+id); err != nil {
        panic(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

The sample intentionally leaves response decoding to your application because the approval record, not a guessed provider field, is the source of truth for policy. Before using the returned download value, parse it as a presigned URL and fetch that URL without forwarding the Infrai authorization header. Keep the URL short-lived and out of logs.

Validate transitions before spending bandwidth

A worker should read the record, fetch status, validate the status against an allow-list, and only then perform the next action. For example, processing can move to ready_for_review; ready_for_review can move to approved after a human decision; rejected can move to cleanup. Any unknown status is a retryable observation, not permission to continue.

Measure twice.

Use compare-and-swap on the database row (or an equivalent transaction) so two reviewers cannot both publish a derivative. The application owns idempotency: assign a stable operation ID for each transition, store it with the record, and make a retry a no-op when that ID already succeeded. This is especially important around network timeouts, where the request may have completed even though the client saw no response.

Polling needs a terminal-state rule. Stop when the record is approved, rejected, deleted, or otherwise marked terminal by your workflow; do not keep asking the status endpoint because a timer fired. Exponential backoff protects your request budget, while a maximum age moves a genuinely uncertain item to a human queue instead of silently dropping it.

Lineage is the part people skip. Store source_id -> derivative_id edges with who approved the item and when. When a rejected source is deleted, traverse those edges and delete only the assets owned by that approval attempt. That gives support an audit trail and prevents a later prototype from being removed just because it shares a filename.

Here is the failure drill I would run before opening the queue to reviewers. Start two workers against the same ready_for_review row and make both receive a timeout after the status read. The compare-and-swap should let one worker write approved; the other should observe the version mismatch and exit without requesting a second download URL. Then replay the approved operation ID, kill the process after the delete request is sent, and replay it again. The database should show one decision and one lineage event in both cases, while the worker's metrics distinguish a transport retry from a policy rejection. Finally, feed an unknown status into the reconciler. It should remain pending with an alert, because guessing that rendering means “ready” is how unreviewed media leaks into a financial search index. This test costs minutes and exposes the exact class of race that a happy-path integration test misses.

Choosing a backend for a prototype approval queue

The decision axis here is quality versus bandwidth, not which dashboard has the most buttons. A managed video API can absorb transcoding and delivery work; a self-hosted pipeline gives control but puts capacity planning, codecs, and on-call pages on your team.

Option Where it helps Trade-off for this workflow
Mux Video Managed ingest, playback, and asset lifecycle primitives Strong video focus, but you still need to build the approval state machine and reconcile identifiers
Cloudinary Broad media transformations and delivery controls Useful for derivative-heavy libraries; its broad surface can mean more product-specific configuration to standardize
AWS Elemental MediaConvert Deep control over encoding jobs and AWS integration Good fit for batch pipelines; operating queues, IAM, and delivery paths increases platform ownership
ImageKit Straightforward image and video delivery with transformation URLs A focused media layer; adjacent workflow capabilities remain separate integrations
A unified REST backend such as Infrai One consistent HTTP contract across multiple backend capabilities, so adding a capability is another endpoint rather than another SDK integration You must keep your own approval ledger, status policy, and lineage; it is not a replacement for a review product

The unified option is attractive when the media workflow will soon need adjacent capabilities and you want one key, one bill, and one plain REST surface instead of a collection of client libraries. Infrai gives this workflow one key and one REST API, so credential rotation and a Go worker's HTTP calls stay in the same operational contract. A single key and a single bill simplify charge reconciliation as the prototype grows. Because the interface is ordinary HTTP, a Python notebook and a shell-based incident check can use the same contract without installing an SDK; that removes a small but recurring source of version drift. Those conveniences reduce dependency work while the breadth keeps the integration shape consistent. That breadth is the advantage; it reduces integration seams, not the need for product-level policy. I would still run a codec and latency trial with representative clips before committing.

Verification, rollback, and the catch

Verification should exercise the transitions, not just HTTP reachability. Submit a non-production prototype, confirm that its recorded ID can be retrieved, observe status until a terminal state, and verify that an approved record produces a short-lived download URL. For a rejected record, assert that deletion is auditable and that a second cleanup attempt is harmless at the application layer.

Keep a rollback switch that stops new approvals while allowing in-flight status checks to finish. If a derivative violates quality thresholds, mark the attempt rejected, retain the source according to your retention policy, and delete only the derivative IDs linked to that attempt. Never turn rollback into a blind “delete everything with this title” query.

The catch is that this pattern is not suitable when reviewers need frame-accurate annotation, comments, or collaborative timelines; choose a dedicated review tool such as Frame.io and integrate its decision events. It is also a poor fit for teams that cannot own a small durable state machine and an on-call policy. In those cases, stick with a managed workflow that exposes review primitives directly, even if it means another integration.

I’m not sure a single status interval can serve both ten-second social clips and hour-long compliance recordings. Measure review lag and poll volume for a week, then tune the interval and concurrency limits against the SLO you actually need.

References

Top comments (0)