Watermarks image assets in Vendure by compositing a mark onto the
preview file the asset server generates. The original source file is never modified,
so the operation is fully reversible.
Compatible with Vendure ^3.0.0.
Vendure stores every asset twice: the untouched source, and a preview (max 1600×1600
by default). Every size the storefront ever requests — thumb, small, medium, large
— is derived by rescaling the preview. Watermarking that one file therefore covers
every rendition, while leaving a pristine original to fall back to.
This is also the only supported extension point: transformImage() in the asset server is
a module-level function, and ImageTransformStrategy can only rewrite transform
parameters, not inject a compositing step.
sharp, @vendure/core and @vendure/asset-server-plugin are peer dependencies. You
already have all three — sharp arrives with the asset server plugin — and keeping it a
peer is deliberate: a second copy of sharp means a second copy of libvips loaded into the
same process.
@aws-sdk/client-s3 is optional, and only needed for S3CachePurgeStrategy.
AssetWatermarkPlugin.init() must be called before previewStrategy().
Then generate the migration for the custom fields the plugin declares:
| Option | Default | Notes |
|---|---|---|
watermarkPath / watermarkBuffer | — | One is required. The image must have an alpha channel. |
enabled | true | Master switch. |
scale | 0.3 | Mark width as a fraction of the image width. |
opacity | 1 | Applied at runtime, so it can be tuned without re-baking the file. |
position | 'southeast' | Nine anchors plus 'tile'. |
margin | 0.03 | Fraction of the width. Corner positions only. |
tileAngle / tileGap | -30 / 0.6 | position: 'tile' only. |
skipMimeTypes | ['image/svg+xml', 'image/gif'] | SVG rasterises unpredictably; GIF would lose its animation. |
minWidth | 200 | Previews narrower than this are left alone. |
previewMaxWidth / previewMaxHeight | — | Passed through to the underlying SharpAssetPreviewStrategy. |
registerCustomFields | true | Set false to skip the two Asset custom fields. |
cachePurgeStrategy | — | Required for the backfill and the opt-out to work correctly. |
scale is a fraction because the asset server rescales the preview down to the presets. A
mark sized in fixed pixels becomes illegible at thumb (150px); a fractional one always
covers the same proportion of the image.
The tiny (50px) and thumb (150px) presets crop with sharp.strategy.entropy, which
keeps the highest-detail region and discards the rest. A corner watermark is frequently
cropped away entirely at those sizes. Use 'center' or 'tile' if thumbnail coverage
matters.
'tile' is also the right choice when the subject carries text you must not obscure —
engraved part numbers, labels, serial plates. A single large centred mark tends to land
right on them.
The plugin adds two fields to the Asset entity:
skipWatermark (boolean) — excludes an asset. Turning it on regenerates a clean
preview from the original; turning it off watermarks it again. It renders in the stock
Admin UI asset detail view, so no UI recompilation is needed.watermarkedAt (datetime, readonly) — the idempotency marker that makes a backfill
resumable.Watermarking cannot be decided per-asset at upload time:
generatePreviewImage()runs before theAssetrow exists and receives no custom fields. Everything supported is therefore watermarked by default, andskipWatermarktriggers an after-the-fact regeneration through the job queue.
The asset server caches each transform at
cache/<preview dir>/<base><md5 of parameters>.<ext>. The md5 covers the transform
parameters, not the file contents — so when a preview is rewritten at the same path,
those keys stay identical and the stale renditions would be served forever.
AssetStorageStrategy exposes no list operation, so purging is delegated to a
CachePurgeStrategy:
S3CachePurgeStrategy — S3 and S3-compatible backends (MinIO, Cloudflare R2,
DigitalOcean Spaces). Needs the optional @aws-sdk/client-s3 dependency.LocalCachePurgeStrategy — the filesystem-backed LocalAssetStorageStrategy.Implement the interface for any other backend:
Asset.source is exposed on the GraphQL type shared by the Admin and Shop APIs, and the
asset server serves /assets/source/* without authentication. The watermark is bypassed
by swapping preview for source in any asset URL.
An optional middleware ships with the plugin:
The leading slash on route matters: beforeListen middleware is applied with a raw
app.use(route, handler), and express will not match a pathless 'assets'.
This is not access control. Checking that a session cookie is present validates neither the session nor any permission. It deters casual scraping. For real protection, block the route at your CDN or load balancer.
Check your storefront does not read asset.source before enabling it.
WatermarkService exposes the pieces:
regeneratePreview always reads the untouched source, so re-running never
double-compresses or double-marks.
By default it overwrites the preview at its current path, which leaves asset.preview
untouched and spares you a search reindex — at the cost of making the cache purge mandatory.
Pass renamePreview: true to write a new file name and update the row instead: that busts
every cache layer at once, which is what you want for a one-off regeneration that is not
followed by a CDN purge.
With backup: true the previous preview is copied under preview-backup/ first, which is
what makes restorePreview() cheap.
Purging your CDN points real traffic at an origin whose derived cache the backfill just
emptied, and every miss makes Vendure run a sharp transform on the spot. Request the
transforms your storefront actually uses against 127.0.0.1 first, then purge — going
through the CDN would be answered from its edge cache and never reach the origin.
Overwriting a preview in place leaves the URL unchanged, so clients that already cached it
keep it until Cache-Control expires — the asset server default is max-age=15552000, i.e.
180 days. A CDN purge clears the edge but not browsers, and nothing can retroactively
shorten an already-issued max-age.
If you need immediate coverage for returning visitors, have the storefront append a version
query param (?v=2). Unknown query params are ignored when the asset server computes its
cache key, so the same file is served under a URL browsers treat as new.
MIT
All notable changes to this project are documented here.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Initial release.
AssetWatermarkPlugin, which composites a watermark onto the preview file the asset
server generates. The original source is never modified, so the operation is reversible.tile, with proportional sizing so the mark stays legible
after the asset server rescales a preview down to the smaller presets.opacity, applied without re-baking the watermark file.CachePurgeStrategy with S3CachePurgeStrategy and LocalCachePurgeStrategy
implementations, for clearing the derived transforms of a regenerated preview.skipWatermark and watermarkedAt custom fields on Asset, for per-asset opt-out and
for tracking a bulk backfill.WatermarkService.regeneratePreview() and restorePreview(), for backfilling an
existing catalogue and rolling it back.createAssetSourceGuard() middleware, which stops the watermark being bypassed
by requesting /assets/source/*.