diff --git a/apps/app/middleware.ts b/apps/app/middleware.ts
index e37a24e2..fab2b91e 100644
--- a/apps/app/middleware.ts
+++ b/apps/app/middleware.ts
@@ -1,6 +1,9 @@
import { authMiddleware } from '@repo/auth/middleware';
+import { noseconeConfig, noseconeMiddleware } from '@repo/security/middleware';
-export default authMiddleware();
+const securityHeaders = noseconeMiddleware(noseconeConfig);
+
+export default authMiddleware(() => securityHeaders());
export const config = {
matcher: [
diff --git a/apps/app/package.json b/apps/app/package.json
index 0c9b256b..4c722372 100644
--- a/apps/app/package.json
+++ b/apps/app/package.json
@@ -14,8 +14,8 @@
},
"dependencies": {
"@prisma/client": "6.0.1",
- "@repo/auth": "workspace:*",
"@repo/analytics": "workspace:*",
+ "@repo/auth": "workspace:*",
"@repo/collaboration": "workspace:*",
"@repo/database": "workspace:*",
"@repo/design-system": "workspace:*",
diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts
index 5d1d703f..f3f41be6 100644
--- a/apps/web/middleware.ts
+++ b/apps/web/middleware.ts
@@ -2,6 +2,7 @@ import { authMiddleware } from '@repo/auth/middleware';
import { env } from '@repo/env';
import { parseError } from '@repo/observability/error';
import { secure } from '@repo/security';
+import { noseconeConfig, noseconeMiddleware } from '@repo/security/middleware';
import { NextResponse } from 'next/server';
export const config = {
@@ -10,9 +11,11 @@ export const config = {
matcher: ['/((?!_next/static|_next/image|ingest|favicon.ico).*)'],
};
+const securityHeaders = noseconeMiddleware(noseconeConfig);
+
export default authMiddleware(async (_auth, request) => {
if (!env.ARCJET_KEY) {
- return NextResponse.next();
+ return securityHeaders();
}
try {
@@ -26,7 +29,7 @@ export default authMiddleware(async (_auth, request) => {
request
);
- return NextResponse.next();
+ return securityHeaders();
} catch (error) {
const message = parseError(error);
diff --git a/docs/features/security/headers.mdx b/docs/features/security/headers.mdx
index b6346479..a1f3618b 100644
--- a/docs/features/security/headers.mdx
+++ b/docs/features/security/headers.mdx
@@ -3,25 +3,106 @@ title: Security Headers
description: Security headers used to protect your application.
---
-next-forge uses [next-secure-headers](https://github.com/jagaapple/next-secure-headers) to set HTTP response headers related to security.
+import { Authors } from '/snippets/authors.mdx';
+
+
+
+next-forge uses [Nosecone](https://docs.arcjet.com/nosecone/quick-start) to set HTTP response headers related to security.
## Configuration
Here are the headers we have enabled:
-| Property | Header | Description | Value |
-| --- | --- | --- | --- |
-| `forceHTTPSRedirect` | `Strict-Transport-Security` | Prevents browsers from connecting to your site over HTTP. | `[true, { maxAge: 63_072_000, includeSubDomains: true, preload: true }]` |
-| `frameGuard` | `X-Frame-Options` | Prevents browsers from rendering your site in an iframe. | `deny` |
-| `noopen` | `X-Download-Options` | Prevents browsers from automatically opening downloaded files in the same origin as the page. | `noopen` |
-| `nosniff` | `X-Content-Type-Options` | Prevents browsers from MIME-sniffing a response away from the declared content type. | `nosniff` |
-| `xssProtection` | `X-XSS-Protection` | Prevents browsers from executing inline scripts if a cross-site scripting attack is detected. | `sanitize` |
-| `contentSecurityPolicy` | `Content-Security-Policy` | Sets a policy to prevent a wide range of different types of attacks, including Cross Site Scripting (XSS) and data injection attacks. | `false` |
-| `expectCT` | `Expect-CT` | Enables a mechanism to mitigate the risk of fraudulent certificates being used in connections to your site. | `false` |
-| `referrerPolicy` | `Referrer-Policy` | Controls how much of the full URL is included in the `Referer` header. | `false` |
+- `Cross-Origin-Embedder-Policy` (COEP)
+- `Cross-Origin-Opener-Policy`
+- `Cross-Origin-Resource-Policy`
+- `Origin-Agent-Cluster`
+- `Referrer-Policy`
+- `Strict-Transport-Security` (HSTS)
+- `X-Content-Type-Options`
+- `X-DNS-Prefetch-Control`
+- `X-Download-Options`
+- `X-Frame-Options`
+- `X-Permitted-Cross-Domain-Policies`
+- `X-XSS-Protection`
-The `forceHTTPSRedirect` property has been customized from the default to include subdomains and preload the HSTS policy. This should allow you to submit your site at [hstspreload.org](https://hstspreload.org/) without any issues.
+See the [Nosecone reference](https://docs.arcjet.com/nosecone/reference) for details on each header and configuration options.
## Usage
-The headers are enabled by default when using the `next-config` package. If you are customizing your `next.config.ts` file, you can extend the headers manually.
+Recommended headers are set by default and configured in `@repo/security/middleware`. Changing the configuration here will affect all apps.
+
+They are then attached to the response within the middleware in `apps/app/middleware` and `apps/web/middleware.ts`. Adjusting the configuration in these files will only affect the specific app.
+
+## Content Security Policy (CSP)
+
+The CSP header is not set by default because it requires specific configuration based on the next-forge features you have enabled.
+
+In the meantime, you can set the CSP header using the Nosecone configuration. For example, the following CSP configuration will work with the default next-forge features:
+
+```ts
+import type { NoseconeOptions } from '@nosecone/next';
+import { defaults as noseconeDefaults } from '@nosecone/next';
+
+const noseconeOptions: NoseconeOptions = {
+ ...noseconeDefaults,
+ contentSecurityPolicy: {
+ ...noseconeDefaults.contentSecurityPolicy,
+ directives: {
+ ...noseconeDefaults.contentSecurityPolicy.directives,
+ scriptSrc: [
+ // We have to use unsafe-inline because next-themes and Vercel Analytics
+ // do not support nonce
+ // https://github.com/pacocoursey/next-themes/issues/106
+ // https://github.com/vercel/analytics/issues/122
+ //...noseconeDefaults.contentSecurityPolicy.directives.scriptSrc,
+ "'self'",
+ "'unsafe-inline'",
+ "https://www.googletagmanager.com",
+ "https://*.clerk.accounts.dev",
+ "https://va.vercel-scripts.com",
+ ],
+ connectSrc: [
+ ...noseconeDefaults.contentSecurityPolicy.directives.connectSrc,
+ "https://*.clerk.accounts.dev",
+ "https://*.google-analytics.com",
+ "https://clerk-telemetry.com",
+ ],
+ workerSrc: [
+ ...noseconeDefaults.contentSecurityPolicy.directives.workerSrc,
+ "blob:",
+ "https://*.clerk.accounts.dev"
+ ],
+ imgSrc: [
+ ...noseconeDefaults.contentSecurityPolicy.directives.imgSrc,
+ "https://img.clerk.com"
+ ],
+ objectSrc: [
+ ...noseconeDefaults.contentSecurityPolicy.directives.objectSrc,
+ ],
+ // We only set this in production because the server may be started
+ // without HTTPS
+ upgradeInsecureRequests: process.env.NODE_ENV === "production",
+ },
+ },
+}
+```
+
diff --git a/packages/next-config/index.ts b/packages/next-config/index.ts
index 71214f21..b83f7dd9 100644
--- a/packages/next-config/index.ts
+++ b/packages/next-config/index.ts
@@ -6,7 +6,6 @@ import { env } from '@repo/env';
import { withSentryConfig } from '@sentry/nextjs';
import withVercelToolbar from '@vercel/toolbar/plugins/next';
import type { NextConfig } from 'next';
-import { createSecureHeaders } from 'next-secure-headers';
const otelRegex = /@opentelemetry\/instrumentation/;
@@ -39,22 +38,6 @@ const baseConfig: NextConfig = {
];
},
- // biome-ignore lint/suspicious/useAwait: headers is async
- async headers() {
- return [
- {
- source: '/(.*)',
- headers: createSecureHeaders({
- // HSTS Preload: https://hstspreload.org/
- forceHTTPSRedirect: [
- true,
- { maxAge: 63_072_000, includeSubDomains: true, preload: true },
- ],
- }),
- },
- ];
- },
-
webpack(config, { isServer }) {
if (isServer) {
config.plugins = [...config.plugins, new PrismaPlugin()];
diff --git a/packages/next-config/package.json b/packages/next-config/package.json
index cd4b4689..13bb1050 100644
--- a/packages/next-config/package.json
+++ b/packages/next-config/package.json
@@ -18,7 +18,6 @@
"@next/bundle-analyzer": "^15.1.0",
"@prisma/nextjs-monorepo-workaround-plugin": "^6.0.1",
"@sentry/nextjs": "^8.43.0",
- "@vercel/toolbar": "^0.1.28",
- "next-secure-headers": "^2.2.0"
+ "@vercel/toolbar": "^0.1.28"
}
}
diff --git a/packages/security/middleware.ts b/packages/security/middleware.ts
new file mode 100644
index 00000000..d32625e9
--- /dev/null
+++ b/packages/security/middleware.ts
@@ -0,0 +1,23 @@
+import {
+ type NoseconeOptions,
+ defaults,
+ withVercelToolbar,
+} from '@nosecone/next';
+import { env } from '@repo/env';
+export { createMiddleware as noseconeMiddleware } from '@nosecone/next';
+
+// Nosecone security headers configuration
+// https://docs.arcjet.com/nosecone/quick-start
+const noseconeOptions: NoseconeOptions = {
+ ...defaults,
+ // Content Security Policy (CSP) is disabled by default because the values
+ // depend on which Next Forge features are enabled. See
+ // https://docs.next-forge.com/features/security/headers for guidance on how
+ // to configure it.
+ contentSecurityPolicy: false,
+};
+
+export const noseconeConfig: NoseconeOptions =
+ env.NODE_ENV === 'development' && env.FLAGS_SECRET
+ ? withVercelToolbar(noseconeOptions)
+ : noseconeOptions;
diff --git a/packages/security/package.json b/packages/security/package.json
index b551a6a9..4a9cdc2c 100644
--- a/packages/security/package.json
+++ b/packages/security/package.json
@@ -2,6 +2,7 @@
"name": "@repo/security",
"version": "0.0.0",
"private": true,
+ "type": "module",
"scripts": {
"clean": "git clean -xdf .cache .turbo dist node_modules",
"format": "biome lint --write .",
@@ -10,6 +11,7 @@
},
"dependencies": {
"@arcjet/next": "1.0.0-alpha.34",
+ "@nosecone/next": "1.0.0-alpha.34",
"@repo/env": "workspace:*"
},
"devDependencies": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 153f7e5c..614fc6f2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -918,9 +918,6 @@ importers:
'@vercel/toolbar':
specifier: ^0.1.28
version: 0.1.28(next@15.1.0(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)(vite@5.4.10(@types/node@22.10.1)(terser@5.31.0))
- next-secure-headers:
- specifier: ^2.2.0
- version: 2.2.0
devDependencies:
'@repo/env':
specifier: workspace:*
@@ -987,6 +984,9 @@ importers:
'@arcjet/next':
specifier: 1.0.0-alpha.34
version: 1.0.0-alpha.34(@bufbuild/protobuf@1.10.0)(@connectrpc/connect@1.6.1(@bufbuild/protobuf@1.10.0))(next@15.1.0(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))
+ '@nosecone/next':
+ specifier: 1.0.0-alpha.34
+ version: 1.0.0-alpha.34(next@15.1.0(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))
'@repo/env':
specifier: workspace:*
version: link:../env
@@ -2844,6 +2844,12 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
+ '@nosecone/next@1.0.0-alpha.34':
+ resolution: {integrity: sha512-4KXgjR580QTsdbM/F9WoLR/AQxJD48na27XnS9De5LIFBemDZ5ObC6L901WrBF3EJGz1M0E9AFQ0nKGUh+ascQ==}
+ engines: {node: '>=18'}
+ peerDependencies:
+ next: '>=14'
+
'@octokit/auth-token@2.5.0':
resolution: {integrity: sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==}
@@ -7813,10 +7819,6 @@ packages:
resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
engines: {node: '>= 0.4.0'}
- next-secure-headers@2.2.0:
- resolution: {integrity: sha512-C7OfZ9JdSJyYMz2ZBMI/WwNbt0qNjlFWX9afUp8nEUzbz6ez3JbeopdyxSZJZJAzVLIAfyk6n73rFpd4e22jRg==}
- engines: {node: '>=10.0.0'}
-
next-themes@0.4.4:
resolution: {integrity: sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==}
peerDependencies:
@@ -7957,6 +7959,10 @@ packages:
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
engines: {node: '>=0.10.0'}
+ nosecone@1.0.0-alpha.34:
+ resolution: {integrity: sha512-GPqypMOeGXjZXHS+I9jTBFB12GNlAmC21LRRhAKuc4v2beMDzW/ChYSQ8sMSr0f7/Ov4ffQJH0i84iAny5Y5fg==}
+ engines: {node: '>=18'}
+
npm-run-path@2.0.2:
resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==}
engines: {node: '>=4'}
@@ -12125,6 +12131,11 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.17.1
+ '@nosecone/next@1.0.0-alpha.34(next@15.1.0(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))':
+ dependencies:
+ next: 15.1.0(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+ nosecone: 1.0.0-alpha.34
+
'@octokit/auth-token@2.5.0':
dependencies:
'@octokit/types': 6.41.0
@@ -18170,8 +18181,6 @@ snapshots:
netmask@2.0.2: {}
- next-secure-headers@2.2.0: {}
-
next-themes@0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
react: 19.0.0
@@ -18367,6 +18376,8 @@ snapshots:
normalize-range@0.1.2: {}
+ nosecone@1.0.0-alpha.34: {}
+
npm-run-path@2.0.2:
dependencies:
path-key: 2.0.1