Production-ready Strapi 5 plugin that automatically applies image watermarks to uploads in the Media Library.
released July 11, 2026
npm install @andrey.tsirkun/strapi-plugin-watermark
Production-ready Strapi 5 plugin that automatically applies image watermarks to uploads in the Media Library.
Works transparently for editors: upload an image as usual, the plugin intercepts the upload pipeline, composites a watermark in memory with Sharp, and sends the result to your configured upload provider.
keepOriginal mode with an extra formats.watermarked variantInstall from npm:
npm install @andrey.tsirkun/strapi-plugin-watermark sharpOr with yarn:
yarn add @andrey.tsirkun/strapi-plugin-watermark sharpEnable the plugin in config/plugins.ts:
export default {
watermark: {
enabled: true,
config: {
enabledByDefault: false,
defaultOpacity: 80,
defaultMargins: { x: 20, y: 20 },
defaultSize: 20,
},
},
};Rebuild the admin panel and restart Strapi:
npm run build
npm run developIf you develop from source, link the package into your Strapi app and enable it the same way:
watermark: {
enabled: true,
resolve: './packages/plugin-watermark',
},config/plugins.ts)These values seed the singleton settings document on first boot:
| Option | Type | Default | Description |
|---|---|---|---|
enabledByDefault | boolean | false | Initial value for the Enabled setting |
defaultOpacity | number | 80 | Initial opacity (0–100) |
defaultMargins | { x, y } | { x: 20, y: 20 } | Initial margin in pixels |
defaultSize | number | 20 | Initial watermark width as % of image width |
Example:
watermark: {
enabled: true,
config: {
enabledByDefault: true,
defaultOpacity: 70,
defaultMargins: { x: 16, y: 16 },
defaultSize: 25,
},
},| Setting | Description |
|---|---|
| Enabled | Master switch for watermark processing |
| Watermark image | PNG/JPEG from Media Library used as the watermark |
| Size | Watermark width as % of the target image width |
| Opacity | 0–100%, applied to the watermark alpha channel |
| Position | One of 9 positions (top/center/bottom × left/center/right) |
| Margins | Horizontal and vertical offset in pixels |
| Process formats | Choose which generated formats receive a watermark |
| Keep original | Keep the untouched original and add formats.watermarked |
| Minimum image size | Skip when both width and height are below the threshold |
| Allowed MIME types | Restrict processing to selected image types |
| Settings page | Watermark picker |
|---|---|
![]() | ![]() |
Admin UI (Settings → Watermark)
│
▼
Admin API GET/PUT /watermark/settings
│
▼
SettingsService ──► plugin::watermark.settings (singleType)
│
Media Library upload
│
▼
UploadInterceptorService (wraps upload provider upload/replace)
│
▼
WatermarkService
├── ValidationService
├── ImageService
├── SharpService
└── CoordinateService
│
▼
Upload provider (local / S3 / …)| Service | Responsibility |
|---|---|
SettingsService | Singleton settings CRUD |
ValidationService | MIME, size, format, and enabled checks |
CoordinateService | Watermark x/y placement with margins |
SharpService | Resize, opacity, composite |
ImageService | Load watermark file from any provider |
WatermarkService | Orchestrates the processing pipeline |
UploadInterceptorService | Hooks the Strapi upload lifecycle |
Strapi Upload optimizes images and calls the upload provider (upload / replace) for each format (original, thumbnail, responsive sizes). Document middleware runs too late to modify buffers before storage.
This plugin registers in bootstrap and wraps plugin::upload provider methods:
WatermarkService.applyWatermark() when validation passesBecause interception happens at the provider layer, it works with any Strapi upload provider without provider-specific code.
keepOriginal behaviorkeepOriginal | processOriginal | Result |
|---|---|---|
false | true | Original file is replaced with the watermarked version |
true | true | Original stays untouched; extra formats.watermarked is uploaded |
| any | false | Original format is not watermarked |
Any provider compatible with Strapi 5 Upload is supported, including:
@strapi/provider-upload-aws-s3)upload / uploadStream and optional getStreamThe plugin reads watermark source files via the provider getStream API when available, with local filesystem and HTTP fallbacks.
Admin routes (require authenticated admin with plugin permissions):
GET /watermark/settingsReturns current watermark settings.
Response
{
"data": {
"enabled": true,
"watermarkImage": 1,
"sizePercent": 20,
"opacity": 80,
"position": "bottom-right",
"marginX": 20,
"marginY": 20,
"processOriginal": true,
"processThumbnail": false,
"processSmall": true,
"processMedium": true,
"processLarge": true,
"keepOriginal": false,
"minimumImageSize": 0,
"allowedMimeTypes": ["image/jpeg", "image/png", "image/webp"]
}
}PUT /watermark/settingsUpdates settings. Accepts { "data": { ... } } or a flat body.
Permissions
plugin::watermark.settings.readplugin::watermark.settings.updateSuper Admin receives these permissions on plugin registration.
From the monorepo root:
# Install dependencies
npm install
# Build the plugin
npm run build:plugin
# Run Strapi test app with plugin watch
npm run dev:allOpen the admin panel, go to Settings → Watermark, configure the watermark image and options, then upload images via Media Library.
packages/plugin-watermark/
├── admin/ Admin panel (Settings page, hooks, components)
├── server/ Plugin server (services, routes, content-types)
├── dist/ Build output (published to npm)
└── package.json# Full CI-style check (from monorepo root)
npm run verify:all
# Individual steps
npm run test:unit --workspace=@andrey.tsirkun/strapi-plugin-watermark
npm run build:plugin
npm run test:integration --workspace=@andrey.tsirkun/strapi-plugin-watermark
npm run test:ts --workspace=@andrey.tsirkun/strapi-plugin-watermark
npm run verify --workspace=@andrey.tsirkun/strapi-plugin-watermarkUpdate version in package.json
Build the plugin:
npm run build --workspace=@andrey.tsirkun/strapi-plugin-watermarkVerify:
npm run verify --workspace=@andrey.tsirkun/strapi-plugin-watermarkPublish from the plugin directory:
cd packages/plugin-watermark
npm publish --access publicPublished tarball includes dist/, README.md, and LICENSE.
Share your work with the community and get it listed in the Strapi ecosystem for everyone to discover and use.
Submit
