Short answer: for white-label image delivery, process a review preset and its watermarks at upload, then generate other formats on demand behind a policy and an authorization check.
Process images at upload for anything that can embarrass a carrier or expose customer data; defer expensive derivatives until demand is real. That split is the practical answer for white-label logistics portals, where presets, watermarks, and formats are part of a publishing policy rather than a collection of buttons.
The deciding constraint is reversibility. A bad crop or an unreadable watermark can be regenerated. A private delivery photo accidentally published to a public tracking page is much harder to undo. I build RAG and agent features in Python, so I treat this like an evaluation problem: define the contract, make a small fixture set, and measure the decisions before scaling the pipeline.
How should white-label portals choose image presets, watermarks, and formats?
Start with three representations: an original kept behind authorization, a review derivative for staff, and a delivery derivative for the branded portal. The upload path can validate type, dimensions, orientation metadata, and malware policy, then create the review image. On-demand work can create a large zoom or a rarely used print format later.
This policy keeps a carrier's logo and a customer's brand separate. Store watermark instructions as data (position, opacity, scale, and text), not as pixels baked into the original. A tenant can then change its mark without asking the system to reconstruct a source file.
Here is a deliberately boring contract for a Python worker. The endpoint is an internal interface; its value is that every tenant receives the same decision fields.
from dataclasses import dataclass
from typing import Literal
@dataclass(frozen=True)
class ImagePolicy:
preset: Literal["review", "portal", "original"]
output_format: Literal["avif", "webp", "jpeg", "png"]
watermark: bool
max_width: int
def policy_for(stage: str, has_alpha: bool) -> ImagePolicy:
if stage == "review":
return ImagePolicy("review", "png" if has_alpha else "webp", True, 1600)
if stage == "portal":
return ImagePolicy("portal", "webp", True, 2400)
return ImagePolicy("original", "png" if has_alpha else "jpeg", False, 0)
The important part is not the enum. It is the audit trail: policy version, tenant, source hash, requested derivative, and the reason the derivative was generated. Without those fields, a support engineer cannot explain why one shipment photo has a mark and another does not.
Upload-time processing, on-demand processing, or a measured split?
Upload-time processing gives predictable review latency and catches unsafe dimensions before a file enters the publishing queue. Its cost is paid even when a shipment is canceled. On-demand processing reduces wasted work, but a first viewer can become the person waiting for a resize, a color conversion, and a cache fill.
I once assumed that a single “process everything” queue would be simpler. It made the queue simple and the product confusing: a ten-megapixel inspection shot competed with a tiny proof-of-delivery thumbnail. The fix was two queues with separate service objectives, plus a small fixture set that includes transparent PNGs, rotated phone photos, and a 20 MB JPEG. Your mileage may vary; the right boundary depends on traffic shape and retention rules.
Measure these before changing the split:
- p95 time from upload acknowledgement to review-ready image.
- Derivative bytes per uploaded byte, by tenant and preset.
- Cache hit rate for portal views and the percentage of derivatives never requested.
- Rejection reasons for dimensions, MIME type, and orientation metadata.
Three short words matter: Do the measurement.
Formats are a negotiation, not a default
The browser's Accept header is a useful hint, not a guarantee that every intermediary behaves perfectly. Keep a canonical source, choose a supported output, and send the matching Content-Type; do not infer a format from a filename alone. AVIF and WebP can reduce transfer size, while JPEG remains a practical fallback for broad compatibility. PNG still earns a place when lossless pixels or alpha transparency are required. MDN's format guide is a good compatibility reference, but test the actual browsers and embedded webviews used by drivers.
Content negotiation also belongs in cache keys. A response that varies by Accept needs an explicit Vary: Accept policy at the edge, or one client's representation can be served to another. Signed URLs should carry tenant and asset scope, and their expiry should match the portal's session model.
Watermarks, privacy, and the failure modes that matter
A watermark is a presentation layer, not access control. A determined viewer can crop it, and a leaked original bypasses it entirely. Authorization must happen before the derivative URL is issued. For public tracking pages, use a different preset that removes sensitive fields and applies a tenant-approved mark.
The subtle failures are operational. EXIF orientation can make a correctly sized image appear sideways. A watermark anchored to raw pixels can drift when the preset changes. A retry that is not idempotent can create duplicate derivatives and inflate storage. Use a source digest plus policy version as the idempotency key, and record the transform library version alongside the result.
Keep the policy narrow. A portal that needs forensic originals, private review, and public thumbnails should not expose one “download image” permission for all three. The catch is that this design adds metadata and queue plumbing; it is not suitable when a team only needs a private folder with manual downloads. Stick with a simpler object store and one access boundary in that case.
Before shipping, run the same fixtures through every preset and assert properties instead of eyeballing screenshots. Check dimensions, alpha handling, orientation, watermark placement, cache headers, and authorization decisions. Keep golden outputs for a few representative files, but allow a tolerance for encoder differences.
def check_result(result: dict, expected: dict) -> None:
assert result["content_type"] == expected["content_type"]
assert result["width"] <= expected["max_width"]
assert result["tenant_id"] == expected["tenant_id"]
assert result["watermark"] is expected["watermark"]
Token cost is not the only budget. Image bytes, queue time, cache churn, and support investigations all show up on the bill. I would rather spend a little compute on a review derivative than make an agent summarize a shipment whose photo policy was never recorded.
Keep tests cheap. Run them often. A long, clause-heavy integration test still matters when it proves that a tenant cannot fetch another tenant's original after a cache miss.
Top comments (0)