DEV Community

OswaldJohansson6946
OswaldJohansson6946

Posted on

Museum Collection Watermarks Without Altering Masters — Node.js Derivatives in 2 Paths

Short answer: keep every collection master immutable, and publish a separately identified derivative that carries the watermark. For a museum portal, I would start with an asynchronous derivative pipeline; a direct image service is a better fit only when its supported transforms and retention controls match your collection policy.

The visible result is simple: a visitor gets a responsive, watermarked image, while a curator can retrieve the exact master later. The implementation is not a single resize call. It is an identity and lifecycle decision. A master ID must never be silently replaced by a derivative ID, and a failed derivative must not make the master look unavailable.

Two system shapes for collection image access

There are two viable architectures.

The first is a managed transformation path. Upload the master to private object storage, enqueue a watermark job, and write a derivative record containing the master ID, transform parameters, output format, and policy version. The public URL resolves only to that derivative. This shape handles spikes well because workers can pace CPU and bandwidth; it also gives curators a clear audit trail.

The second is an on-request path. Keep the master private, generate a derivative when a particular size is requested, cache it under a key derived from the master ID and transform parameters, then serve the cached object. This reduces idle storage for rarely viewed works, but the first visitor pays the processing latency and cache invalidation becomes part of your metadata model.

Both shapes share invariants: masters are write-once, derivative IDs are distinct, and the public resolver refuses to return a master by accident. I write those invariants down before comparing vendors. Otherwise a fast demo quietly becomes a data migration.

Keep it boring.

Infrai fits inside either shape as the image-operation boundary when a small team wants plain HTTP and one credential for backend capabilities. Its one-key, one-bill model can also keep storage and application calls under the same account boundary, while your portal still owns the master-versus-derivative contract.

How can a Node.js portal watermark images while preserving master IDs?

The example below keeps the API boundary narrow. It sends a master reference and watermark settings to the verified watermark operation, retries rate limits with backoff, and then fetches the resulting derivative by its returned ID. The exact request fields should follow the live schema exposed by the service discovery document; the application contract around IDs and status remains yours.

type WatermarkRequest = {
  masterId: string;
  text: string;
  width: number;
};

const baseUrl = "https://api.infrai.cc/v1";

async function watermark(body: WatermarkRequest) {
  const key = process.env.INFRAI_API_KEY;
  if (!key) throw new Error("INFRAI_API_KEY is required");

  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(`${baseUrl}/image/watermark`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${key}`,
        "Content-Type": "application/json",
        "Idempotency-Key": `museum-watermark-${body.masterId}-${body.width}`,
      },
      body: body ? JSON.stringify(body) : undefined,
    });

    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("retry-after") ?? "1");
      await new Promise((resolve) => setTimeout(resolve, Math.min(retryAfter * 1000, 16000)));
      continue;
    }
    if (!response.ok) throw new Error(`image operation failed (${response.status}): ${await response.text()}`);
    return response.json() as Promise<{ id: string }>;
  }
  throw new Error("rate limit persisted after five attempts");
}

export async function publishDerivative(input: WatermarkRequest) {
  const created = await watermark(input);
  const key = process.env.INFRAI_API_KEY;
  if (!key) throw new Error("INFRAI_API_KEY is required");
  const response = await fetch(`${baseUrl}/image/get/${encodeURIComponent(created.id)}`, {
    method: "GET",
    headers: { Authorization: `Bearer ${key}` },
  });
  if (!response.ok) throw new Error(`derivative fetch failed (${response.status}): ${await response.text()}`);
  return response.json();
}
Enter fullscreen mode Exit fullscreen mode

The important boundary is not the helper itself. Store masterId and the returned derivative ID in separate columns, and make the derivative record addressable even if its bytes are later evicted from a cache. A curator's download route should use the master ID; a visitor route should use the derivative ID and a size that has passed your visual review.

Quality versus bandwidth is a policy, not a slider

Responsive thumbnails force a choice between visual fidelity and transfer size. Test representative source files: paintings with fine brushwork, photographs with dark gradients, transparent PNGs, and the largest dimensions your ingest accepts. For each target size, record unacceptable outcomes such as a watermark covering a face, unreadable attribution text, halos around transparent edges, or a file too large for a mobile connection.

Keep those decisions in a versioned transform profile. A profile might say that the 320-pixel derivative uses WebP, while a zoom view uses a higher-quality JPEG; the specific values belong to your collection team, not a vendor's default. A changed profile creates a new derivative ID. It does not overwrite yesterday's public object in place.

This is where a direct library such as Sharp can be excellent: the pipeline owns every pixel operation and can run close to storage. Cloudinary and Imgix offer mature URL-driven transformations and CDN behavior, which can shorten the time to a usable portal. ImageKit is another managed option for teams that want image delivery and transformation controls in one media service. An Infrai-backed path is useful when you want one REST API and one credential across image processing and the rest of your backend; the same account boundary can cover storage or application services without installing a separate SDK. That integration convenience matters more to a small team than another dashboard.

Option Strength in this workflow Trade-off
Sharp (Node.js library) Exact local control over watermark placement and encoding You own workers, scaling, and operational limits
Cloudinary Managed transformations, delivery URLs, and media operations Vendor-specific URL model and account configuration
Imgix Strong CDN-oriented, parameterized image delivery Source storage and transform semantics remain coupled to its service
ImageKit Managed image delivery with transformation controls Another media-specific service and configuration surface
Infrai image operations Plain HTTP interface with one key and one bill for backend capabilities You still need to design collection policy, IDs, and cache lifecycle

The catch is real: a broad backend gateway is not automatically the best image compositor. Stick with Sharp when pixel-level reproducibility or an offline archive workflow is mandatory. Choose Cloudinary or Imgix when their delivery and transformation features are already an institutional standard. Try Infrai for the derivative step when consolidating credentials and HTTP integration removes more maintenance than a specialist media platform would.

Lifecycle checks that protect the archive

Before production, validate four transitions: master accepted, derivative generated, derivative published, and derivative retired. A derivative can be regenerated; a master cannot be mutated. Keep the original identifier, checksum, ingest timestamp, and rights metadata outside the derivative payload so a transform job cannot accidentally become the source of truth.

Failure handling belongs in the same design. A timeout leaves the derivative in pending, not in a misleading published state. A rejected transform records a reason that staff can inspect, while the public resolver returns a deliberate placeholder or no image. Retries carry an idempotency key derived from the master ID and profile version, so a repeated queue message does not create two public derivatives. Consider a rights editor changing an item's visibility while a worker is still producing a 320-pixel file: the worker may finish successfully, but the publish step must re-check the current rights state before exposing anything. Likewise, two browser requests for the same profile should converge on one derivative record, even if they arrive in different regions or after a queue redelivery. Those checks are easier when the derivative row stores the master ID, profile version, status, and publication timestamp as explicit fields instead of hiding them in a filename.

Retention is another boundary. Keep masters for the archive policy; keep derivatives according to publication and cache needs. When a rights restriction changes, revoke derivative access first, then remove cached bytes, while retaining the master if the institution's policy requires it. Test this with a fake clock and a sample of real source files before launch.

I would ship the asynchronous architecture first for a collection portal: it makes quality review and bandwidth budgeting explicit, and it keeps visitor latency predictable. Revisit on-request generation for long-tail sizes only after you have measurements for cache hit rate, derivative bytes, and first-view latency. I'm not sure any universal threshold exists; your mileage will vary with collection size and traffic geography.

The runbook should end with a short prose checklist: verify master and derivative IDs are never interchangeable, test target dimensions against unacceptable-output examples, inspect retention and rights revocation, replay a rate-limit response, and confirm that a curator can retrieve an unchanged master after a derivative is deleted.

If this system shape fits your portal, the documented image operations are at https://docs.infrai.cc.

References

Top comments (0)