
By Bret Cameron
Preview unpublished Strapi content from your frontend by sending one HTTP header. Works on REST and GraphQL.
released May 18, 2026
npm install strapi-plugin-draft-previewPreview unpublished Strapi content from your frontend with a single HTTP header, securely.
By default, Strapi's draft mode:
status for every query.find permission as published content. If draft leakage matters, Strapi's built-in system can't help.This plugin solves both issues.
Common use cases:
?status=draft blocked entirely.npm install strapi-plugin-draft-previewEnable the plugin in config/plugins.js:
module.exports = {
"draft-preview": { enabled: true },
};Then send x-include-drafts: true from your frontend. Apollo Client example:
const draftHeaderLink = setContext((_, { headers }) => ({
headers: {
...headers,
...(process.env.NODE_ENV !== "production" && {
"x-include-drafts": "true",
}),
},
}));Now, outside of production, all queries will return drafts!
Whether the plugin honours the header is decided by the auth gate, in this order of priority:
authorize: a custom callback. If you set it, it decides.requireAuth: built-in check. true allows callers authenticated via API token. String forms ("api-token", "admin") pin to one strategy.authorize or requireAuth.| Caller | ?status=draft (native) | x-include-drafts header |
|---|---|---|
| Allowed by gate | drafts | drafts |
Denied by gate, guardNativeStatus: false (default) | drafts (Strapi serves them) | published |
Denied by gate, guardNativeStatus: true | published | published |
If you run separate Strapi instances per environment (one for staging, one for production), this is the default behaviour: ship the plugin, set NODE_ENV=production on the prod instance, and the header is automatically ignored there.
If you run one Strapi instance serving multiple frontends (a shared CMS), use case 2 below is the right pattern instead. The env gate alone won't help: a single CMS in production would deny the header for every frontend, including staging.
"draft-preview": {
enabled: true,
config: { requireAuth: true, guardNativeStatus: true },
},Bake an API token into your preview frontend, send it with Authorization: Bearer <token> plus the preview header. Anyone without the token gets published, including via ?status=draft.
This is also the right shape for one Strapi instance serving multiple frontend environments (prod, UAT, develop, etc.). The token decides who sees drafts, not NODE_ENV.
Issue separate API tokens per environment, allow-list them by name:
authorize: (ctx) =>
["preview-uat", "preview-develop"].includes(
ctx.state.auth?.credentials?.name,
),A leaked token in one environment is recoverable by rotating just that token.
Express it from authorize:
authorize: (ctx) => allowedIps.includes(ctx.ip),For richer rules (IP reputation, rate limits, geo) use your CDN or WAF.
If you genuinely want the header to be public:
authorize: () => true,| Key | Type | Default | Description |
|---|---|---|---|
headerName | string | "x-include-drafts" | HTTP header that flips a request into draft mode. |
expectedHeaderValue | string | "true" | Header value treated as truthy. |
statusValue | string | "draft" | Status string injected into queries when the gate allows. |
authorize | (ctx) => boolean | Promise<boolean> | (unset) | Custom predicate. If set, its return value is the gate's decision. Thrown errors are treated as deny. |
requireAuth | true | "api-token" | "admin" | false | false | Built-in check. true allows callers authenticated via API token or admin JWT; string forms pin to one strategy. |
guardNativeStatus | boolean | false | When set, denied requests using the native ?status=draft (REST) or status: DRAFT (GraphQL) paths are rewritten to published. Without this, the native paths bypass the gate. |
5.x20, 22, 24Upgrading from v1? See CHANGELOG.md for the migration paths.
Contributions welcome. See CONTRIBUTING.md for setup, tests, and the changeset workflow.
MIT
Share your work with the community and get it listed in the Strapi ecosystem for everyone to discover and use.
Submit
