Object Storage Thumbnails: Sharp Workers for Private-Bucket Resizing and Signed URLs
Short answer: for a B2B SaaS that serves private image thumbnails, make tenant and source revision part of the derivative identity, generate thumbnails in a bounded worker, and issue a short-lived signed URL only after an application authorization check. The reliability decision is the isolation boundary, not the resize library. A thumbnail can look perfect and still be the wrong tenant's object. That is the incident lesson I would put at the top of the runbook. The incident starts with a valid thumbnail in the wrong place Consider a media library where one tenant replaces an image while two browser tabs request the old 320-pixel preview. A queue retry and a cache miss arrive close together. The transform succeeds. The output is readable. The failure is that an object key based only on image ID and width can now point at a stale revision, or at an identifier that another tenant can guess. I would test this with a deliberately awkward case: a request for a 4096-pixel derivative, a duplicated enqueue, and a lookup made with the same image ID under a different tenant. The first request should be rejected before Sharp sees it; the duplicate should converge on one work identity; the cross-tenant lookup should fail before the source object is read. A 4096-pixel request is not a capacity plan. It is a boundary test. The invariant is simple. Isolate before processing. The database row, source key, derivative key, and signed-link event should all carry the tenant and source revision. A readable result proves the encoder worked. It does not prove authorization worked. That distinction changes the incident response order. Start with the authenticated tenant, then compare the work row, original object key, derivative key, and URL-issuance event using one correlation ID. Only after those identities agree should an operator inspect dimensions, format, or transform duration. This produces a useful audit trail even when the image pipeline reports success. How should a Node.js worker shape private object storage thumbnails? Keep the web request responsible for policy and the worker responsible for bytes and CPU. The request authenticates the caller, resolves the tenant, verifies ownership of the source image, allowlists width and format, and creates or finds a work record. The Node.js worker can use Sharp to fetch that authorized source, enforce pixel and byte budgets, resize it, write a deterministic derivative, and mark the work ready. The web path then signs the derivative rather than proxying the media through the application. The worker is not an authorization boundary. Queue delivery retries, deployments overlap, and callers can repeat the same request, so the work identity needs all of (tenant, image, revision, width, format). Explicit states such as queued, processing, ready, and failed make recovery inspectable. Here is the storage contract I would keep behind the application policy layer. It is provider-neutral; the production transformation can remain a Node.js and Sharp concern while the same identity checks are enforced around it. package thumbnail import ( "context" "errors" "fmt" "time" ) type Store interface { Get(ctx context.Context, key string) ([]byte, error) Put(ctx context.Context, key string, body []byte, contentType string) error SignedGet(ctx context.Context, key string, expires time.Duration) (string, error) } type Work struct { Tenant string Image string Revision string Width int Format string } func derivativeKey(w Work) string { return fmt.Sprintf("tenants/%s/images/%s/derivatives/%s/w%d.%s", w.Tenant, w.Image, w.Revision, w.Width, w.Format) } func BuildThumbnail(ctx context.Context, store Store, w Work) (string, error) { if w.Tenant == "" || w.Image == "" || w.Revision == "" || w.Width < 16 || w.Width > 2048 { return "", errors.New("invalid thumbnail request") } key := derivativeKey(w) if _, err := store.Get(ctx, key); err == nil { return store.SignedGet(ctx, key, 10*time.Minute) } else if !errors.Is(err, context.Canceled) { return "", err } return "", errors.New("thumbnail is not ready") } The example leaves ownership lookup outside Store on purpose. The caller must establish that the authenticated principal may use w.Tenant before this function runs, and a worker must verify that its work row still names the same tenant and revision before it writes. A deterministic key is useful for convergence; it is not a substitute for a database uniqueness rule or an authorization check. The catch is that on-demand generation is not suitable when the first-view SLO cannot tolerate a cold miss. Pre-generate the small set of variants the product can justify, or accept the queue latency as part of the user-visible contract. Reliability comes from identities, not retries Retries are necessary, but retrying the wrong work item more reliably is still a failure. Use an idempotent scheduling rule around the full work identity, and make a replacement create a new source revision. That keeps an old worker from overwriting a current derivative after a retry or deployment overlap. A signed URL is a time-limited delivery capability, not continuing proof that the recipient is entitled to the object. Recheck ownership when minting the link. Set the expiry to match the delivery policy, and remember that expiry does not revoke a URL already copied by a browser or CDN during its valid window. The derivative can remain durable after the link expires; another authorized request can receive a new link. The object prefix helps humans inspect storage, but it cannot carry the whole security model. Store credentials on the server, keep the source and derivative namespaces distinct, and test that a guessed key is denied by the application even if the object exists. My preferred failure drill is small: enqueue the same work twice, replace the source before the second attempt, and inspect which revision each worker reads and writes. Then issue a link, let its ten-minute policy lapse, and verify that an authorized request can mint a fresh one while an unauthorized tenant still cannot. I'm not sure every team needs three formats on day one; your mileage may vary, and the variant catalog should follow actual screen requests rather than fashion. What should the SLO and capacity budget measure? The useful SLO is the time from a cache miss to a usable thumbnail, not the time to acknowledge a queue message. Track thumbnail hit ratio, queue age, transform duration, time from miss to ready, signed-link issuance latency, authorization denials, and requests waiting for a derivative. Break them down by tenant and source size. A healthy global average can hide one large customer consuming the worker pool. Capacity planning starts with variants. Five widths and two formats can create ten derivatives per source revision, before retries and replacements. Estimate decoded pixels, output bytes, queue concurrency, and temporary memory per transform. A worker can show modest CPU while the first-view SLO is already failing because the queue is waiting behind oversized inputs. Put lifecycle rules beside the revision policy. Deleting old derivatives quickly reduces the inventory, but can complicate rollback and audit; keeping every revision is an explicit capacity commitment. The right retention period depends on product recovery and audit needs, so measure the derivative catalog before selecting it. Object lifecycle management is an operational policy, not a janitorial script. Which delivery boundary survives a tenant-isolation review? The comparison should start with control ownership and on-call coverage. Storage price is one input, not the decision. Choice Fits when Trade-off to accept Managed object storage plus a queue The team wants durable bytes with less storage maintenance The application still owns isolation, derivative state, workers, and recovery tests Self-hosted object storage Data placement and protocol control outweigh maintenance work The team owns upgrades, disk recovery, replication, and on-call coverage Resize on upload The variant catalog is small and predictable New sizes require reprocessing, while unused variants consume capacity Resize on demand Traffic is uneven and many variants would be wasteful A cold miss adds queue latency and needs a clear SLO Application-proxied delivery Per-request policy cannot be represented by a signed URL The application becomes a bandwidth and egress bottleneck Signed direct delivery Private objects can be served with expiring links Link leakage remains possible during the expiry window Stick with application-proxied delivery when every byte needs a policy decision that an expiring link cannot express, and accept the bandwidth cost deliberately. Self-hosting is a poor fit when the platform team cannot staff replication and disk recovery. Resize on upload is a poor fit when the product keeps adding unpredictable variants. The decision rule is conditional: bind tenant and revision to every derivative, make duplicate work converge, bound the transform, observe the miss-to-ready SLO, and sign only after authorization. That rule can survive a storage migration because the security and reliability contracts live above the provider. Further reading https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html https://www.backblaze.com/cloud-storage/pricing
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to