Docs
Next.js Plugin
Security headers and dependency monitoring for Next.js. One wrapper, zero config.
Installation
Install the plugin with your package manager of choice:
npm install @withpanache/nextjs
# or
pnpm add @withpanache/nextjs
# or
yarn add @withpanache/nextjsQuick start
Wrap your Next.js config with withPanache. That's it: your app now has production-ready security headers and X-Powered-By is disabled.
// next.config.ts
import { withPanache } from "@withpanache/nextjs"
export default withPanache({
// your existing Next.js config
})Security headers
By default, withPanache injects the following headers on all routes. These are permissive enough for most Next.js apps while passing Panache's security checks.
| Header | Default value |
|---|---|
Strict-Transport-Security | max-age=63072000; includeSubDomains |
Content-Security-Policy | default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'self' |
X-Content-Type-Options | nosniff |
X-Frame-Options | SAMEORIGIN |
Referrer-Policy | strict-origin-when-cross-origin |
Permissions-Policy | camera=(), microphone=(), geolocation=() |
Dynamic CSP at runtime
Next.js freezes the headers() function into the build manifest at next build. If a CSP directive depends on an environment variable that only exists at deploy time (a CDN domain, a reports origin, a per-tenant value), a build-time CSP would be missing that source: the header is captured before the variable is set, and the served policy silently drops the origin. The fix is to emit the CSP from your middleware (proxy.ts) instead, where runtime env is available. Three steps: 1. Turn off the build-time CSP in withPanache with contentSecurityPolicy: false, and turn off build-time CSP reporting with cspMonitoring: false. This keeps the static headers (HSTS, X-Frame-Options, Referrer-Policy, Permissions-Policy, X-Content-Type-Options) at build time while releasing the CSP. 2. Your middleware becomes the single owner of the Content-Security-Policy header. 3. Import buildCspHeaders and setSecurityHeaders from @withpanache/nextjs/headers. This subpath is Edge-safe (zero node: imports). Build the header list once at module load: nothing varies per request, so reading process.env there is correct and avoids rebuilding on every request. setSecurityHeaders uses Headers.set, guaranteeing a single CSP header.
// next.config.ts
import { withPanache } from "@withpanache/nextjs"
export default withPanache(nextConfig, {
// Keep the static security headers at build time, but release the CSP
// so the middleware can emit it from runtime env.
security: { contentSecurityPolicy: false },
// Disable build-time CSP reporting injection too, otherwise a second
// Content-Security-Policy header would be emitted at build.
cspMonitoring: false,
})
// proxy.ts (or middleware.ts)
import { buildCspHeaders, setSecurityHeaders } from "@withpanache/nextjs/headers"
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
// Frozen once at module load: nothing varies per request, so REPORTS_DOMAIN
// (a runtime env var, empty at build) is read here, not at next build.
const CSP_HEADERS = buildCspHeaders({
dev: process.env.NODE_ENV !== "production",
contentSecurityPolicy: {
"frame-src": ["'self'", process.env.REPORTS_DOMAIN],
},
})
export default function proxy(request: NextRequest) {
const response = NextResponse.next()
// .set guarantees a single Content-Security-Policy header.
setSecurityHeaders(response.headers, CSP_HEADERS)
return response
}Customizing headers
Pass a security object to override or disable individual headers. Set a header to false to skip it entirely.
export default withPanache(nextConfig, {
security: {
// Disable HSTS (e.g. behind a reverse proxy)
strictTransportSecurity: false,
// Custom referrer policy
referrerPolicy: "no-referrer",
// Stricter framing
xFrameOptions: "DENY",
},
})Content Security Policy
CSP can be configured as a typed object or a raw string. Object form: your sources are added to the defaults (deduplicated). You don't need to repeat 'self' or other default values. Directives you don't mention keep their default values. To replace a specific directive instead of extending it, use the replace() helper. String form: fully replaces the entire default CSP. All forms have full TypeScript autocomplete for directive names and source values.
import { withPanache, replace } from "@withpanache/nextjs"
export default withPanache(nextConfig, {
security: {
contentSecurityPolicy: {
// Extends: sources are added to the defaults
"script-src": ["https://cdn.example.com"],
// Replaces: this directive fully overrides the default
"connect-src": replace(["'self'", "https://api.example.com"]),
},
},
})
// String form: replaces the entire CSP
export default withPanache(nextConfig, {
security: {
contentSecurityPolicy: "default-src 'none'; script-src 'self'",
},
})CSP monitoring
Opt-in client-side CSP violation reporting. Generate a CSP token in the dashboard (Settings > CSP) and the plugin will: 1. Add `report-to panache` to your Content-Security-Policy header (with a legacy `report-uri` fallback for Safari ≤17 and older Firefox). 2. Emit a matching `Reporting-Endpoints: panache="…"` header so browsers POST violations directly to Panache. Reports are aggregated hourly and visible in the dashboard. CSP monitoring is disabled if no token is provided. Learning mode switches the enforcing `Content-Security-Policy` header to `Content-Security-Policy-Report-Only`: violations are reported but not blocked. Useful while you discover what your existing site actually loads before turning enforcement on. Reports from non-default branches are tagged with the branch name (detected via CI_COMMIT_BRANCH, GITHUB_REF_NAME, VERCEL_GIT_COMMIT_REF, CF_PAGES_BRANCH, or local git). Set `defaultBranch` if your project does not use `main`. For self-hosted Panache deployments, set `cspMonitoring.endpoint` (or PANACHE_CSP_ENDPOINT) to point at your own ingest URL. Default: https://csp.withpanache.dev
// Recommended: env vars
// PANACHE_CSP_TOKEN=<csp-token-from-dashboard>
// PANACHE_CSP_LEARNING_MODE=1 # optional, switches to Report-Only
// Or via plugin options
export default withPanache(nextConfig, {
cspMonitoring: {
token: process.env.PANACHE_CSP_TOKEN,
learningMode: false,
// endpoint: "https://csp.your-self-hosted.example", // optional
},
defaultBranch: "main", // override if your default is not "main"
})Permissions Policy
Same pattern as CSP. Object form adds to defaults, string form replaces. An empty array disables the feature.
export default withPanache(nextConfig, {
security: {
permissionsPolicy: {
// Merge with defaults (camera, microphone, geolocation stay disabled)
fullscreen: ["self"],
payment: [],
},
},
})How merging works
If you already define a headers() function in your Next.js config, Panache appends its headers to your catch-all route without overriding any header you've set. If you don't have a headers() function, Panache creates one. Your headers always take priority. Panache never overwrites an existing header. In development, the default CSP includes 'unsafe-eval' in script-src because React's development build requires it. This is automatically removed in production.
Dependency monitoring
During next build, the plugin detects your package manager, collects the full dependency tree, and pushes it to Panache for vulnerability tracking. This runs in a detached subprocess and never blocks or slows down your build. If it fails, it fails silently. Supported package managers: pnpm, npm, yarn (classic and berry). To enable it, add your site token:
export default withPanache(
{ /* next config */ },
{ token: process.env.PANACHE_SITE_TOKEN }
)CI/CD detection
The plugin automatically detects git SHA, branch, and preview URL from common CI environments: Git SHA: GITHUB_SHA, CI_COMMIT_SHA, VERCEL_GIT_COMMIT_SHA, CF_PAGES_COMMIT_SHA, or local git. Branch: GITHUB_REF_NAME, CI_COMMIT_BRANCH, VERCEL_GIT_COMMIT_REF, CF_PAGES_BRANCH, or local git. Preview URL: VERCEL_URL, CF_PAGES_URL, DEPLOY_URL. No configuration needed. If the environment variables are present, they are used automatically.
All options
The full options reference:
interface PanacheOptions {
/** Site token from the Panache dashboard. Also reads PANACHE_SITE_TOKEN env var. */
token?: string
/** Ingest API URL. Default: "https://withpanache.dev/api/v1/ingest" */
apiUrl?: string
/** Security headers. true = all defaults, false = skip, object = customize. Default: true */
security?: boolean | SecurityHeadersConfig
/** Default branch. Drives branch-aware features (per-branch CSP reports, etc.). Default: "main" */
defaultBranch?: string
/** CSP violation reporting. Disabled if no token is provided. */
cspMonitoring?: {
/** CSP token from the dashboard. Also reads PANACHE_CSP_TOKEN. */
token?: string
/** Switches to Content-Security-Policy-Report-Only. Also reads PANACHE_CSP_LEARNING_MODE. Default: false */
learningMode?: boolean
/** Override the report endpoint base URL. Also reads PANACHE_CSP_ENDPOINT. */
endpoint?: string
}
}TypeScript
All types are exported for use in your config:
import type {
PanacheOptions,
SecurityHeadersConfig,
CspDirectives,
CspDirectiveName,
CspSourceValue,
CspMonitoringConfig,
PermissionsPolicyDirectives,
PermissionsPolicyFeature,
ReferrerPolicy,
} from "@withpanache/nextjs"Need help?
If you have questions about the plugin or need assistance, reach out at hello@withpanache.dev