A plug-and-play Strapi 5 plugin that gives editors manual control over cache invalidation directly from the admin UI. It abstracts away the cache implementation — Varnish, Redis proxy APIs, CDN purge APIs, or any HTTP-based cache system can be configured as a provider without touching plugin code.
- Document Action — "Purge Cache" in the edit view dropdown and in list-view table row actions. Purges the cache entry for the specific content being edited.
- Bulk Action — "Purge Cache (N)" in the list-view bulk action toolbar. Deduplicates paths across all selected entries before purging, so
/newsis only called once even if 20 articles are selected. - Settings Dashboard — Settings > Cache Manager shows all configured providers, their available endpoints, live connection statistics, per-provider "Purge" buttons, a "Purge All Caches" control, and a read-only view of the content type mapping configuration.
- Provider-agnostic — Communicates with any HTTP-accessible cache system. All connection details live in
config/plugins.ts. - Environment variable interpolation — Use
${VAR_NAME}in any URL or header value. Resolved at request time fromprocess.env, so credentials are never hardcoded. - Internationalized — Ships with English and German translations. Additional languages can be added by dropping a JSON file in
admin/src/translations/.
Strapi Admin UI
├── Edit view → Document Action ("Purge Cache") ─┐
├── List view → Bulk Action ("Purge Cache (N)") ├──▶ Admin API (authenticated) ──▶ Cache Service ──▶ Provider HTTP endpoints
└── Settings → Cache Manager Dashboard ─┘ /cache-manager/* (server) (Varnish, CDN, ...)
When an editor triggers a purge:
- The admin UI calls one of the plugin's server-side routes, authenticated via the Strapi admin JWT.
- The server fetches the content entry from Strapi's document service to get its field values (slug, etc.).
- The cache service resolves which frontend URL paths need purging, using the content type mapping defined in config.
- For each resolved path, the service calls the configured provider endpoints over HTTP — for example, Varnish's
/varnish-purgeand/varnish-banendpoints. - Results are aggregated and returned. The
successfield reflects whether at least one provider call succeeded.
The contentTypeMapping config tells the plugin how to translate a Strapi content type entry into frontend URL paths. For example:
api::post.post + { slug: "my-post" }
→ pathPattern "/blog/{slug}" → "/blog/my-post"
→ relatedPaths → "/blog", "/"
→ purge: ["/blog/my-post", "/blog", "/"]
The {fieldName} placeholder in pathPattern is replaced with the actual value of that field from the entry. Any top-level entry field can be used — {slug}, {locale}, {id}, etc.
Content types with purgeAllOnChange: true skip path resolution entirely and ban everything (useful for global content like headers, footers, and navigation menus).
For each path, the plugin calls both the purge and ban endpoints on providers that have them configured:
purge(POST /varnish-purgewithX-Purge-URL): Removes the exact cached object immediately. Fast and precise.ban(POST /varnish-banwithX-Ban-URL): Adds a ban expression to Varnish's ban list. Invalidates any object whose URL matches the pattern, including any grace-period or stale objects thatpurgemight miss.
Together they provide thorough cache invalidation: purge handles the immediately cached object, ban handles any lingering stale variants.
- Strapi 5.x
- Node 22+
npm install @leancoders/strapi-plugin-cache-manager
# or
pnpm add @leancoders/strapi-plugin-cache-manager
# or
yarn add @leancoders/strapi-plugin-cache-managerIn config/plugins.ts:
export default ({ env }) => ({
'cache-manager': {
enabled: true,
config: {
providers: [
{
name: 'Varnish',
type: 'http',
endpoints: {
purge: {
url: '${VARNISH_URL}/varnish-purge',
method: 'POST',
headers: { 'X-Purge-Token': '${VARNISH_PURGE_TOKEN}' },
pathParam: 'X-Purge-URL',
pathLocation: 'header',
},
ban: {
url: '${VARNISH_URL}/varnish-ban',
method: 'POST',
headers: { 'X-Purge-Token': '${VARNISH_PURGE_TOKEN}' },
pathParam: 'X-Ban-URL',
pathLocation: 'header',
},
purgeAll: {
url: '${VARNISH_URL}/varnish-ban',
method: 'POST',
headers: {
'X-Purge-Token': '${VARNISH_PURGE_TOKEN}',
'X-Ban-URL': '.',
},
},
},
},
],
contentTypeMapping: {
'api::post.post': {
pathPattern: '/blog/{slug}',
relatedPaths: ['/blog', '/'],
},
'api::page.page': {
pathPattern: '/{slug}',
},
'api::footer.footer': {
purgeAllOnChange: true,
},
'api::tag.tag': {
relatedPaths: ['/blog'],
},
},
},
},
});# .env
VARNISH_URL=http://localhost:6081 # local dev (Varnish exposed on port 6081)
VARNISH_PURGE_TOKEN=your-secret-token # must match the token Varnish is configured withIn production (Docker), use VARNISH_URL=http://varnish (the container name).
pnpm developNavigate to Settings > Cache Manager to verify providers are showing.
{
name: string; // Display name shown in the dashboard
type: string; // Informational type tag (e.g. 'http')
endpoints: {
purge?: ProviderEndpoint; // Called for each path on single/bulk purge
ban?: ProviderEndpoint; // Called for each path on single/bulk purge (Varnish ban list)
purgeAll?: ProviderEndpoint; // Called when "Purge All" is triggered
stats?: ProviderEndpoint; // Called on dashboard load to show statistics
};
}All endpoints are optional. A provider with only purgeAll is valid. A provider with only stats is valid.
{
url: string; // Base URL — supports ${VAR_NAME} interpolation
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
headers?: Record<string, string>; // Extra headers — supports ${VAR_NAME} interpolation
params?: Record<string, string>; // Static query parameters added to every request
pathParam?: string; // Name of the param or header that receives the path value
pathLocation?: 'query' | 'header'; // Where the path value is sent (default: 'query')
}When purging a specific URL (e.g. /news/my-article), the plugin needs to tell the cache system which URL to purge. Different systems expect this in different places:
Via query parameter (pathLocation: 'query', the default):
GET http://my-cache/purge?path=/news/my-article
^^^^
pathParam: 'path'
Via HTTP header (pathLocation: 'header'):
POST http://varnish/varnish-purge
X-Purge-URL: /news/my-article
^^^^^^^^^^^^^^^^^
pathParam: 'X-Purge-URL'
contentTypeMapping: {
'api::post.post': {
pathPattern: '/blog/{slug}', // {fieldName} replaced with entry field value
relatedPaths: ['/blog', '/'], // Additional paths purged alongside the entry path
},
'api::footer.footer': {
purgeAllOnChange: true, // Triggers purgeAll instead of path-based purge
},
'api::tag.tag': {
relatedPaths: ['/blog'], // No pathPattern — only related paths are purged
},
}| Field | Type | Description |
|---|---|---|
pathPattern |
string |
URL pattern — {fieldName} is replaced with entry[fieldName] |
relatedPaths |
string[] |
Extra paths purged alongside the entry path (e.g. listing pages) |
purgeAllOnChange |
boolean |
When true, triggers a full cache purge instead of specific paths |
Field substitution examples:
pathPattern: '/blog/{slug}' + entry.slug = 'my-post' → '/blog/my-post'
pathPattern: '/{locale}/blog/{slug}' + entry.locale = 'en', entry.slug = 'my-post' → '/en/blog/my-post'
pathPattern: '/products/{slug}' + entry.slug = 'my-product' → '/products/my-product'
Any top-level field on the Strapi entry can be used as a placeholder.
Use ${VAR_NAME} anywhere in a URL, header value, or param value:
url: '${VARNISH_URL}/varnish-purge' // resolved at request time
headers: { 'X-Purge-Token': '${VARNISH_PURGE_TOKEN}' }Resolution happens at request time (not at startup), so:
- Credentials are never exposed in the config or the database
- The admin UI never receives URLs or header values (only provider names and endpoint keys)
- A missing
${VAR_NAME}resolves to an empty string (does not crash Strapi)
Varnish accepts purge and ban via custom HTTP endpoints defined in default.vcl:
{
name: 'Varnish',
type: 'http',
endpoints: {
purge: {
url: '${VARNISH_URL}/varnish-purge',
method: 'POST',
headers: { 'X-Purge-Token': '${VARNISH_PURGE_TOKEN}' },
pathParam: 'X-Purge-URL',
pathLocation: 'header',
},
ban: {
url: '${VARNISH_URL}/varnish-ban',
method: 'POST',
headers: { 'X-Purge-Token': '${VARNISH_PURGE_TOKEN}' },
pathParam: 'X-Ban-URL',
pathLocation: 'header',
},
purgeAll: {
url: '${VARNISH_URL}/varnish-ban',
method: 'POST',
headers: {
'X-Purge-Token': '${VARNISH_PURGE_TOKEN}',
'X-Ban-URL': '.', // regex '.' matches every URL
},
},
},
}Required env vars: VARNISH_URL, VARNISH_PURGE_TOKEN
How it works: The VCL checks X-Purge-Token on every purge/ban request. If it matches, the operation proceeds; otherwise Varnish returns 403. The purgeAll endpoint bans everything by sending X-Ban-URL: . (regex that matches all URLs).
Redis has no native HTTP API, so you expose a small endpoint in your frontend app (Next.js, Astro, Express, etc.) that handles Redis operations and call it from the plugin.
Example frontend endpoint (/api/cache):
GET /api/cache?action=clear-path&path=/blog/my-post → deletes the Redis key for that path
GET /api/cache?action=stats → returns key counts / memory usage
DELETE /api/cache → flushes all cached keys
Plugin config:
{
name: 'Redis Cache',
type: 'http',
endpoints: {
purge: {
url: '${FRONTEND_URL}/api/cache',
method: 'GET',
headers: { Authorization: 'Bearer ${CACHE_API_TOKEN}' },
params: { action: 'clear-path' },
pathParam: 'path',
pathLocation: 'query', // sends path as ?path=/blog/my-post
},
purgeAll: {
url: '${FRONTEND_URL}/api/cache',
method: 'DELETE',
headers: { Authorization: 'Bearer ${CACHE_API_TOKEN}' },
},
stats: {
url: '${FRONTEND_URL}/api/cache',
method: 'GET',
headers: { Authorization: 'Bearer ${CACHE_API_TOKEN}' },
params: { action: 'stats' },
},
},
}Required env vars: FRONTEND_URL, CACHE_API_TOKEN
How it works: The plugin calls your frontend's HTTP endpoint for each cache operation. The endpoint translates the request into the appropriate Redis command (DEL for a specific key, FLUSHDB for purge-all, etc.). The Authorization header prevents unauthorized cache clears.
{
name: 'Cloudflare',
type: 'http',
endpoints: {
purgeAll: {
url: 'https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/purge_cache',
method: 'POST',
headers: { 'Authorization': 'Bearer ${CF_API_TOKEN}' },
},
},
}Note: Cloudflare's purge API requires a JSON body (
{ "purge_everything": true }). The current plugin version does not send request bodies. Per-URL purge is not yet supported for APIs that require body payloads. See Extending the Plugin.
All providers are called for every operation:
providers: [
{ name: 'Varnish', type: 'http', endpoints: { purge: {...}, ban: {...}, purgeAll: {...} } },
{ name: 'Redis Cache', type: 'http', endpoints: { purge: {...}, purgeAll: {...}, stats: {...} } },
]Purging one post calls Varnish's purge + ban for each path, and in parallel calls the Redis endpoint's purge for each path — both caches are invalidated in a single editor action.
All routes are served under Strapi's admin API prefix and require a valid admin JWT token (admin::isAuthenticatedAdmin policy). They are not accessible from the public Strapi REST/GraphQL API.
| Method | Path | Body / Query | Description |
|---|---|---|---|
| GET | /cache-manager/providers |
— | List providers (name, type, endpoint keys only — no URLs or credentials) |
| GET | /cache-manager/stats |
?provider=Name (optional) |
Fetch stats from providers that have a stats endpoint |
| GET | /cache-manager/content-type-mapping |
— | Return the full content type mapping config |
| POST | /cache-manager/purge-entry |
{ contentTypeUid, documentId } |
Purge cache for one entry |
| POST | /cache-manager/purge-bulk |
{ contentTypeUid, documentIds[] } |
Purge cache for multiple entries (paths deduplicated) |
| POST | /cache-manager/purge-all |
{ provider? } (optional) |
Purge all caches (optionally scoped to one provider) |
Every purge route returns:
{
"success": true,
"results": [
{
"provider": "Varnish",
"endpoint": "purge",
"success": true,
"status": 200,
"message": "purge succeeded on Varnish",
"details": null
},
{
"provider": "Varnish",
"endpoint": "ban",
"success": true,
"status": 200,
"message": "ban succeeded on Varnish",
"details": null
}
]
}success at the top level is true when at least one result succeeded and there is at least one result. details contains the parsed JSON response body from the cache provider (if any).
Failure example (wrong token):
{
"success": false,
"results": [
{
"provider": "Varnish",
"endpoint": "purge",
"success": false,
"status": 403,
"message": "purge failed on Varnish: HTTP 403",
"details": null
}
]
}Failure example (network error):
{
"success": false,
"results": [
{
"provider": "Varnish",
"endpoint": "purge",
"success": false,
"message": "purge failed on Varnish: fetch failed",
"details": null
}
]
}cache-manager/
├── package.json strapi plugin package (kind: plugin)
├── README.md
├── ARCHITECTURE.md
│
├── admin/
│ ├── custom.d.ts ambient type declarations
│ ├── tsconfig.json
│ ├── tsconfig.build.json
│ └── src/
│ ├── index.tsx plugin registration: document action, bulk action, settings link
│ ├── pluginId.ts PLUGIN_ID constant: 'cache-manager'
│ ├── api.ts authenticated fetch wrappers (getFetchClient)
│ ├── components/
│ │ ├── Initializer.tsx marks plugin as ready after registration
│ │ └── PluginIcon.tsx trash icon used in actions
│ ├── pages/
│ │ └── Settings.tsx dashboard: providers table, stats, purge-all dialog
│ ├── translations/
│ │ ├── en.json English strings
│ │ └── de.json German strings
│ └── utils/
│ └── getTranslation.ts prefixes translation keys with 'cache-manager.'
│
└── server/
├── tsconfig.json
├── tsconfig.build.json
└── src/
├── index.ts server entry: wires config, bootstrap, controllers, routes, services
├── register.ts (empty — no custom fields)
├── bootstrap.ts logs configured provider names on startup
├── destroy.ts (empty — no cleanup needed)
├── config/
│ └── index.ts default config (providers: [], contentTypeMapping: {}) + validator
├── controllers/
│ ├── index.ts
│ └── cache-controller.ts validates input, fetches entries, delegates to service
├── routes/
│ ├── index.ts declares route group as type 'admin'
│ └── admin-api.ts 6 route definitions with isAuthenticatedAdmin policy
├── services/
│ ├── index.ts
│ └── cache-service.ts core logic: env var resolution, path resolution, HTTP execution
├── content-types/
│ └── index.ts (empty — no custom content types)
├── middlewares/
│ └── index.ts (empty)
└── policies/
└── index.ts (empty — uses built-in admin policy)
Translation keys follow this naming convention:
| Key prefix | Where used |
|---|---|
cache-manager.settings.* |
Settings dashboard page |
cache-manager.action.purge.* |
Single-entry document action |
cache-manager.action.purge-bulk.* |
Bulk action |
To add a new language, create admin/src/translations/{locale}.json following the same key structure as en.json. The registerTrads function in index.tsx discovers it automatically via dynamic import.
Some APIs (e.g. Cloudflare) require a JSON body. To support this:
- Add
body?: Record<string, unknown>to theProviderEndpointinterface incache-service.ts. - In
executeEndpoint, passbody: endpoint.body ? JSON.stringify(endpoint.body) : undefinedto thefetch()call. - In
plugins.ts, configure the endpoint with abodyfield.
All providers currently use the 'http' type. To add a provider that communicates differently (e.g. via native Redis commands):
- Add a type guard in
executeEndpointthat checksprovider.type. - Implement the custom logic path for the new type.
- The
ProviderConfig.typefield can remain freeform — it's informational only.
The plugin is currently manual-only — editors trigger purges. To add automatic purging when content is published:
- In
server/src/bootstrap.ts, register lifecycle hooks usingstrapi.db.lifecycles.subscribe. - In the
afterUpdate/afterPublishhandlers, callstrapi.plugin('cache-manager').service('cache-service').purgeEntry(uid, entry). - This would make cache invalidation transparent to editors.
Plugin doesn't appear in Settings:
- Check
enabled: trueinconfig/plugins.ts - Verify the package is installed:
npm ls @leancoders/strapi-plugin-cache-manager - Look for
[cache-manager]messages in Strapi startup logs
"fetch failed" on purge:
- The cache provider isn't running or isn't reachable at the configured URL
- In local dev: start Docker (
docker compose up -d) and verify your cache service is up - Test the endpoint directly, e.g.:
curl -X POST http://localhost:6081/varnish-purge -H "X-Purge-Token: your-token" -H "X-Purge-URL: /"
HTTP 403 on purge:
VARNISH_PURGE_TOKENin your.envdoesn't match the token your cache provider expects- Restart the container after changing the token:
docker compose up -d --force-recreate varnish
"Invalid URL" on purge:
VARNISH_URL(or whichever URL env var you're using) is not set in your.env- The
${VAR_NAME}interpolation resolves to an empty string, makingnew URL('')throw - Set it to the correct address, e.g.
http://localhost:6081(local dev) or the container name in production
Document action not showing in edit view:
- The action hides itself when
documentIdordocumentis null (e.g. when creating a new entry before first save) - Check browser console for JavaScript errors
- Verify the content-manager plugin loads before cache-manager
Bulk action not showing:
- Select at least one entry in the list view — the action only appears in the bulk selection toolbar