Supabase Studio Scored 93. Here's Their next.config.
Supabase Studio Scored 93. Here's Their next.config.
Supabase Dashboard scored 93 on our Security Headers & Basics audit — an A grade, and the highest score we've seen from a Next.js project in our benchmarks. Most Next.js apps score between 45 and 70 on this audit. Supabase nearly aced it.
The reason isn't complicated. They configured their security headers properly in next.config.js. That's it. No special middleware. No third-party security SDK. Just the headers config that Next.js has supported since version 10 and that almost nobody uses.
The headers that matter
Here's what a production-grade Next.js headers configuration looks like, modeled on what the highest-scoring projects get right:
// next.config.js
const securityHeaders = [
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=(), browsing-topics=()',
},
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self';",
},
];
Let's walk through each one.
Header-by-header breakdown
Strict-Transport-Security
max-age=63072000; includeSubDomains; preload
This tells browsers: "Always use HTTPS for this domain. For the next 2 years. Including all subdomains. And add us to the browser's built-in preload list."
The max-age of 63072000 seconds (2 years) is the recommended value for production. Shorter values like 86400 (1 day) are fine for testing but too weak for production — an attacker only needs one HTTP request to intercept traffic.
includeSubDomains is critical if you serve anything on subdomains (API, docs, admin panels). Without it, api.yourdomain.com could still be accessed over HTTP.
preload lets you submit your domain to the HSTS preload list maintained by browsers. Once preloaded, browsers will never make an HTTP request to your domain — not even the first one. This closes the one remaining gap in HSTS where the initial request could be intercepted.
In our audit, this check (security-headers.transport.hsts-enabled) is high severity with a weight of 3.
X-Frame-Options
SAMEORIGIN
Prevents your site from being embedded in an <iframe> on another domain. This blocks clickjacking attacks where an attacker overlays your real UI with a transparent iframe and tricks users into clicking things they didn't intend to.
SAMEORIGIN allows your own domain to iframe itself (useful for previews, embedded components) while blocking external embedding. Use DENY if you never need iframing.
Note: CSP's frame-ancestors directive is the modern replacement, but X-Frame-Options is still needed for older browsers that don't support CSP.
X-Content-Type-Options
nosniff
One word, huge impact. Without this header, browsers will try to "sniff" the MIME type of responses, which can lead to a text file being interpreted as JavaScript and executed. This is a medium-severity check in our audit, but it's the easiest fix on the list — there's never a reason not to set it.
Referrer-Policy
strict-origin-when-cross-origin
Controls how much URL information is sent in the Referer header when navigating away from your site. The options, from most to least private:
no-referrer— sends nothing (breaks some analytics and CSRF protection)strict-origin-when-cross-origin— sends full URL for same-origin, only the origin for cross-origin, nothing for HTTPS-to-HTTPorigin— always sends just the origin (domain), never the path
strict-origin-when-cross-origin is the sweet spot. Your own analytics see full URLs. Third-party sites only see your domain name. No referrer data leaks over insecure connections.
Permissions-Policy
camera=(), microphone=(), geolocation=(), browsing-topics=()
This header restricts which browser APIs your site can access. The empty parentheses () mean "deny to all origins, including self."
Unless your app actually uses the camera, microphone, or geolocation, disable them. This prevents any injected script from accessing these APIs — even if an attacker gets past your CSP.
browsing-topics=() opts you out of Google's Topics API (the Privacy Sandbox replacement for third-party cookies). There's no reason to allow it unless you're running ads.
Other useful restrictions to consider:
payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()
This check (security-headers.headers.permissions-policy) is low severity in our audit, but it's one of the most commonly missed headers. Almost no AI-generated project sets it.
Content-Security-Policy
default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; ...
CSP is the big one. It controls which resources can be loaded and executed on your pages. A strict CSP prevents cross-site scripting (XSS) even if an attacker finds an injection point.
The ideal CSP uses nonces or hashes instead of 'unsafe-inline':
script-src 'self' 'nonce-{random}';
But most Next.js apps need 'unsafe-inline' for styled-jsx or CSS-in-JS, and 'unsafe-eval' for development mode. The pragmatic approach: use 'unsafe-inline' for styles (hard to avoid with most CSS frameworks) but try to eliminate it for scripts.
Our audit checks both csp-present (is there any CSP at all?) and csp-no-unsafe-inline (is the CSP actually strict?). Both are high severity. Most projects fail the second one — having a CSP that allows 'unsafe-inline' for scripts is better than no CSP, but it significantly weakens protection.
The copy-paste template
Here's the complete next.config.js headers configuration. Copy it, adjust the CSP directives for your specific third-party services, and deploy:
/** @type {import('next').NextConfig} */
const nextConfig = {
poweredByHeader: false,
productionBrowserSourceMaps: false,
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload',
},
{
key: 'X-Frame-Options',
value: 'SAMEORIGIN',
},
{
key: 'X-Content-Type-Options',
value: 'nosniff',
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin',
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=(), browsing-topics=()',
},
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline'", // Remove unsafe-inline if possible
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https:",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"upgrade-insecure-requests",
].join('; '),
},
],
},
];
},
};
module.exports = nextConfig;
Adjust the CSP for your needs:
- Using Google Fonts? Add
https://fonts.googleapis.comtostyle-srcandhttps://fonts.gstatic.comtofont-src - Using Stripe? Add
https://js.stripe.comtoscript-srcandframe-src - Using analytics? Add the analytics domain to
script-srcandconnect-src - Loading images from a CDN? Add the CDN domain to
img-src
What separates a 93 from a 55
The difference between Supabase Dashboard's 93 and the average Next.js project's score is not sophistication — it's configuration. They set the headers. Most projects don't.
Our Security Headers & Basics audit has 21 checks across four categories:
| Category | Weight | What it covers | |----------|--------|---------------| | Transport Security | 30% | HTTPS, HSTS, cookie flags | | Security Headers | 30% | CSP, X-Frame-Options, Referrer-Policy, Permissions-Policy, CORS | | Information Exposure | 25% | Stack traces, server version, source maps, error pages | | Basic Hygiene | 15% | .gitignore, secrets, dependencies, lockfile, security.txt |
Projects that score in the 90s pass nearly everything. Projects that score in the 50s typically have zero custom headers configured — they're running on framework defaults with no security layer.
The headers template above covers the Transport Security and Security Headers categories almost completely. Combine it with poweredByHeader: false (Information Exposure) and proper .gitignore entries (Basic Hygiene), and you're looking at a score north of 85 with about 15 minutes of work.
No security expertise required. No middleware framework. Just a next.config.js that does its job.