DEV Community

sawyerflynn1578
sawyerflynn1578

Posted on

Claim Photo Intake — Auditable Metadata Inspection and Lifecycle Controls

An insurance claim image is evidence before it is media. Short answer: preserve the uploaded bytes, inspect them once into an immutable manifest, and allow background removal or other normalization only on a versioned derivative; lifecycle validation should then prove that every derivative still points to the retained original. This costs more bandwidth than immediately replacing the upload, but it keeps a visual enhancement from silently becoming an evidence mutation.

The governing constraint is therefore auditability, not thumbnail speed. A browser-provided filename, extension, or media type can help route work, yet none of them should be treated as the authoritative description of the bytes. Media formats have distinct container and codec characteristics, as the MDN media formats guide makes clear. Intake has to identify what arrived, record what it decided, and make retries converge on the same record.

Keep the original. Everything else can be rebuilt.

How should insurance claim image metadata inspection shape lifecycle validation at intake?

Treat intake as a small ledger with a media payload attached. The first durable entry represents receipt: a claim-scoped upload identifier, a digest calculated from the received bytes, the byte count, the declared media type, the inspected media type, and the time accepted. A second entry represents policy evaluation. Later entries represent transformations such as orientation correction, thumbnail generation, or background removal. Each entry names its input digest and output digest, so an auditor can follow the chain without trusting mutable object names.

The useful distinction is between claims and observations. photo.jpg and image/jpeg are claims made by a client. A decoder successfully reading dimensions is an observation made by the intake service. A SHA-256 digest is an identity over the exact received byte sequence. These fields shouldn't be collapsed into one convenient type column because they answer different questions during reconciliation. If the extension says JPEG while inspection identifies a different supported format, policy can reject the upload or place it in a review state; it shouldn't quietly rewrite history.

Exactly-once processing is the wrong promise at a network boundary. Exactly-once effects are attainable: derive an idempotency key from the claim, upload session, and content digest; place a uniqueness constraint on that key; and commit the manifest plus the object reference in one controlled state transition. A retry may execute inspection twice, but it must not create two accepted evidence records or launch two derivative pipelines.

Retries are normal.

This is the core contract:

Record Mutable? Purpose
Original object No Evidence received from the claimant
Intake manifest Append-only Declared and observed metadata, digest, decision
Policy decision Append-only Accepted, rejected, or held, with a reason code
Derived object Replace by version Delivery-optimized or background-removed rendition
Lifecycle event Append-only Retention, legal hold, deletion eligibility, deletion result

No single metadata field proves authenticity. The manifest proves something narrower and operationally valuable: which bytes the system received, what its inspector observed, which policy version ran, and what happened next.

Make acceptance atomic and replayable

The handler below sketches the boundary in Go. It reads through a size limit, computes identity from bytes rather than a filename, uses the standard library to inspect the media type, and delegates the atomicity requirement to a repository interface. The storage and database implementations are intentionally generic; the important part is the contract between them.

package intake

import (
    "bytes"
    "context"
    "crypto/sha256"
    "encoding/hex"
    "errors"
    "io"
    "net/http"
)

var ErrTooLarge = errors.New("claim image exceeds intake limit")

type Manifest struct {
    ClaimID       string
    UploadID      string
    DigestSHA256  string
    Bytes         int64
    DeclaredType  string
    InspectedType string
    PolicyVersion string
}

type Repository interface {
    // AcceptOnce atomically binds one immutable object to one idempotency key.
    AcceptOnce(ctx context.Context, idempotencyKey string, body []byte, m Manifest) error
}

func AcceptImage(
    ctx context.Context,
    repo Repository,
    claimID string,
    uploadID string,
    declaredType string,
    policyVersion string,
    maxBytes int64,
    src io.Reader,
) (Manifest, error) {
    limited := io.LimitReader(src, maxBytes+1)
    body, err := io.ReadAll(limited)
    if err != nil {
        return Manifest{}, err
    }
    if int64(len(body)) > maxBytes {
        return Manifest{}, ErrTooLarge
    }

    sum := sha256.Sum256(body)
    digest := hex.EncodeToString(sum[:])
    sample := body
    if len(sample) > 512 {
        sample = sample[:512]
    }

    m := Manifest{
        ClaimID:       claimID,
        UploadID:      uploadID,
        DigestSHA256:  digest,
        Bytes:         int64(len(body)),
        DeclaredType:  declaredType,
        InspectedType: http.DetectContentType(sample),
        PolicyVersion: policyVersion,
    }
    key := claimID + ":" + uploadID + ":" + digest
    if err := repo.AcceptOnce(ctx, key, bytes.Clone(body), m); err != nil {
        return Manifest{}, err
    }
    return m, nil
}
Enter fullscreen mode Exit fullscreen mode

Real deployments should stream large bodies into quarantine storage while hashing them, rather than retaining the full payload in memory. The same invariant still applies: the object isn't available to downstream consumers until its manifest has an accepted decision, and a failed client connection cannot leave a publicly addressable partial object. A reconciliation worker compares manifests with object inventory and reports both directions of drift: a manifest without bytes and bytes without a manifest. Don't make that worker repair records silently; repair is another auditable transition.

Status codes also need stable meaning. A policy rejection is a client-visible decision, not a transient processing failure. A duplicate idempotency key should return the previously committed result when the digest agrees; if the same upload identifier arrives with different bytes, 409 Conflict is a reasonable contract because accepting both would make the claimant's intent ambiguous. The precise response schema is local policy, but its reason code must be stable enough for clients and reconciliation jobs to distinguish retryable transport failures from final intake decisions.

Spend bandwidth where evidence quality changes the decision

Quality versus bandwidth is not one global toggle. It is a sequence of choices. Upload bandwidth protects the original evidence; internal bandwidth moves it through inspection; delivery bandwidth serves an adjuster-friendly rendition. Conflating those stages leads teams to compress too early and then discover that the pixels needed for review existed only on the claimant's device.

For intake, permit only formats that the entire inspection and review path can decode consistently. MDN's guide is a useful catalog of media format characteristics, but a production allowlist must also reflect the decoders actually deployed and tested by the organization. I'm not sure any universal allowlist would be defensible here: browser support, server decoding, security review, and claims policy are separate constraints, and a format passing one does not imply it passes the others.

Then define derivatives by use, not by vague labels such as “optimized.” A review rendition can have a bounded dimension and a known encoding policy. A thumbnail can trade detail for fast list views. A background-removed image may help a tool isolate the claimed product, but it is an analytical aid and must carry source_digest, transform_policy_version, and its own output digest. Never promote it over the original, because edge removal can change the apparent outline of transparent, reflective, or low-contrast items.

Derivatives are expendable.

The bandwidth rule can stay compact: upload the highest-quality original the intake channel permits; inspect before transformation; generate smaller derivatives near the review path; cache by derivative digest; and fetch the original only for workflows whose decision needs it. This allocates bandwidth according to evidentiary value rather than applying the same compression rule everywhere.

There is a catch. Keeping originals plus derivatives increases storage, replication traffic, and deletion work. It is not suitable when the organization has no authority to retain the original, when an explicit collection policy requires immediate minimization, or when legal counsel has defined a shorter retention boundary. In those cases, use a policy-approved minimization step at the trust boundary and record the input/output transition allowed by that policy; do not let an engineering convenience invent the retention period. Compliance limits vary by jurisdiction and line of business, so the retention schedule and legal-hold behavior require written approval from the responsible compliance function.

Validate the lifecycle, not merely the upload

An accepted file can still become an operational liability if lifecycle state is inferred from object-store age alone. Represent lifecycle state explicitly: received, accepted, held, eligible_for_deletion, and deleted are business events, while storage classes and cache residency are implementation details. A legal hold blocks eligibility. Deletion becomes complete only when the policy-defined copies and derivatives are gone and a durable deletion event records which manifest was acted upon.

Tests should attack transitions. Submit the same bytes twice with the same idempotency key and expect one manifest. Reuse the upload identifier with different bytes and expect a conflict. Interrupt a streamed upload and verify that no accepted record is visible. Change the transformation policy and verify that a new derivative version appears while the original digest remains fixed. Put a claim on hold immediately before a deletion worker runs and prove the worker rechecks authority rather than relying on a stale queue message.

Observe invariants, not just latency: count orphan objects, missing objects, duplicate acceptance attempts, digest mismatches, policy rejections by stable reason code, derivatives whose source is absent, and deletion jobs blocked by holds. Logs should carry claim and manifest identifiers but avoid duplicating image bytes or unneeded claimant metadata. Metrics can be aggregated; audit events must remain attributable.

Short-lived systems may not need the full state machine. If images are genuinely non-evidentiary, carry no retention obligation, and can be regenerated from another authoritative source, a simpler overwriteable media pipeline is easier to operate. Stick with that model when those conditions are documented. For claim evidence, the extra manifest and reconciliation work buys a property a generic image pipeline cannot provide: a reviewable history of custody and change.

Roll out the boundary before the transformations

Start in shadow mode: calculate the digest and inspected type, write a candidate manifest, and compare the proposed decision with current intake without changing client-visible behavior. Next, enforce idempotent acceptance for one intake channel, then turn on reconciliation, and only after those records are stable should derivative generation consume accepted manifests. Migrate background removal last, because its outputs are disposable once their source links and policy versions are correct. The rollout gate is simple: every accepted manifest resolves to one immutable original; every derivative resolves to one accepted source digest; every retry converges; every lifecycle action identifies the policy that authorized it. Once those invariants hold, quality and bandwidth can be tuned without weakening the evidence boundary.

References

Top comments (0)