Supabase Auth is one of those tools that feels simple right up until you add a strict Content Security Policy. Then login starts failing in weird ways: OAuth popups stop working, token refresh breaks, realtime disconnects, and you end up loosening your policy until it barely counts as CSP.
I’ve done that mistake before. The fix is not to throw https: everywhere and hope for the best. The fix is to understand exactly what Supabase Auth needs, then allow only that.
This guide covers a practical CSP for Supabase Auth in real apps, including email/password, magic links, OAuth, and browser clients using supabase-js.
What Supabase Auth usually needs from CSP
For a typical browser app using Supabase Auth, these directives matter most:
connect-srcfor API calls to your Supabase projectscript-srcif your app injects inline scripts or uses noncesstyle-srcif your framework injects stylesimg-srcif provider logos or user avatars load from remote URLsframe-srcif your auth flow uses embedded framesform-actionfor auth-related form submissionsbase-uri,object-src, andframe-ancestorsfor baseline hardening
The biggest one by far is connect-src. Supabase browser clients talk to your project over HTTPS and often WebSockets too.
A common Supabase project endpoint looks like this:
https://YOUR_PROJECT_REF.supabase.co
If you use realtime, you’ll also need WebSocket access:
wss://YOUR_PROJECT_REF.supabase.co
If you use a custom domain for Supabase, use that instead.
Start with a sane baseline
Here’s a clean baseline policy for a server-rendered app with Supabase Auth:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self' https://YOUR_PROJECT_REF.supabase.co wss://YOUR_PROJECT_REF.supabase.co;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
That is the version I’d start with before adding anything else.
A couple of opinions here:
- Don’t put your Supabase domain in
default-src. Keep it inconnect-src. - Don’t start with
script-src 'unsafe-inline'. - Don’t allow broad wildcards like
https://*.supabase.counless you truly need them.
The pattern from a real production CSP like the one seen on headertest.com is useful here. Notice how it keeps a strict baseline and grants specific permissions per directive instead of leaning on default-src for everything:
content-security-policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-NjJjNGE0MzMtYjc3Zi00NTEyLTlkMTAtY2QzM2RlYWNhMWJh' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
frame-src 'self' https://consentcdn.cookiebot.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none'
That’s the right mindset for Supabase too: tight baseline, explicit exceptions.
CSP for supabase-js in the browser
If you initialize Supabase in frontend code like this:
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
'https://YOUR_PROJECT_REF.supabase.co',
'YOUR_PUBLIC_ANON_KEY'
)
your browser needs permission to connect to that origin.
Minimum working CSP
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self' https://YOUR_PROJECT_REF.supabase.co;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
If you use Supabase Realtime or any auth flow that upgrades to WebSockets, change connect-src to:
connect-src 'self' https://YOUR_PROJECT_REF.supabase.co wss://YOUR_PROJECT_REF.supabase.co;
I would include the wss: endpoint upfront if you’re using the standard client library. Saves you a future debugging session.
OAuth logins: where CSP usually breaks
Supabase OAuth flows often redirect users to a provider, then back to your app. That redirect itself is usually not blocked by CSP. What breaks is the client-side network traffic before or after the redirect.
A login call like this:
await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: 'https://app.example.com/auth/callback'
}
})
still needs:
connect-src https://YOUR_PROJECT_REF.supabase.co- maybe
form-action 'self'if your app posts local forms - possibly
frame-srcif you embed auth-related pages in frames, which I generally avoid
For plain redirect-based OAuth, you usually do not need to allow the provider domain in connect-src or script-src, because the browser is navigating away, not loading provider scripts into your page.
That distinction matters. People over-allow Google, GitHub, Discord, and half the internet when they only needed their own Supabase project origin.
Magic links and OTP flows
Magic links are mostly easy from a CSP standpoint. The user clicks a link in email, lands on your site, and your app exchanges the credentials with Supabase.
Example:
const { error } = await supabase.auth.verifyOtp({
email,
token,
type: 'email'
})
Or for sign-in with OTP:
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: 'https://app.example.com/auth/callback'
}
})
Again, the key requirement is connect-src to your Supabase project.
If your callback page includes inline script to parse query params and complete auth, that’s where script-src can bite you.
Bad fix:
script-src 'self' 'unsafe-inline';
Better fix: use a nonce or move the script into an external file.
Nonce-based example
<script nonce="{{ .CSPNonce }}">
window.AUTH_REDIRECT_PATH = "/auth/callback";
</script>
<script nonce="{{ .CSPNonce }}" src="/assets/app.js"></script>
CSP:
script-src 'self' 'nonce-{{NONCE}}';
If your framework supports nonces, use them. If not, external scripts are still better than turning on unsafe-inline.
Example: Express app with a Supabase-friendly CSP
Here’s a practical Express setup:
import express from 'express'
import crypto from 'node:crypto'
const app = express()
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64')
res.locals.nonce = nonce
const csp = [
"default-src 'self'",
`script-src 'self' 'nonce-${nonce}'`,
"style-src 'self'",
"img-src 'self' data:",
"font-src 'self'",
"connect-src 'self' https://YOUR_PROJECT_REF.supabase.co wss://YOUR_PROJECT_REF.supabase.co",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'"
].join('; ')
res.setHeader('Content-Security-Policy', csp)
next()
})
app.get('/', (req, res) => {
res.send(`
<!doctype html>
<html>
<head>
<title>Supabase Auth CSP</title>
</head>
<body>
<button id="login">Login with GitHub</button>
<script nonce="${res.locals.nonce}" type="module">
import { createClient } from '/js/supabase-browser.js'
const supabase = createClient(
'https://YOUR_PROJECT_REF.supabase.co',
'YOUR_PUBLIC_ANON_KEY'
)
document.getElementById('login').addEventListener('click', async () => {
await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: 'https://app.example.com/auth/callback'
}
})
})
</script>
</body>
</html>
`)
})
app.listen(3000)
This is the kind of policy I trust in production: specific, boring, and easy to reason about.
Example: Next.js header config
If you’re setting CSP in Next.js, you can do something like this in next.config.js for a static baseline:
const csp = [
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob:",
"font-src 'self'",
"connect-src 'self' https://YOUR_PROJECT_REF.supabase.co wss://YOUR_PROJECT_REF.supabase.co",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
"object-src 'none'"
].join('; ')
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Content-Security-Policy',
value: csp
}
]
}
]
}
}
Why style-src 'unsafe-inline' here? Because some React and Next setups still make strict style CSP annoying. I don’t love it, but I’d rather be honest than pretend every stack can run a perfect policy on day one.
If your app can avoid inline styles, remove it.
Debugging checklist when Supabase Auth fails under CSP
When login suddenly stops working, check these in order:
-
Browser console CSP errors
- This usually tells you the blocked directive and exact URL.
-
connect-src- Missing
https://YOUR_PROJECT_REF.supabase.co - Missing
wss://YOUR_PROJECT_REF.supabase.co
- Missing
-
Inline callback scripts
- Your auth callback page may rely on inline JS.
- Replace with external JS or nonces.
-
Framework-generated styles
- If the UI breaks but auth still works,
style-srcmay be too strict.
- If the UI breaks but auth still works,
-
Custom Supabase domain
- If you use one, whitelist that exact host, not the default project ref host.
-
Overly broad
default-srcassumptionsdefault-srcdoes not magically cover everything once a more specific directive exists.
That last one catches people all the time.
A stricter production-ready example
Here’s a stronger policy for a typical app using Supabase Auth and little else:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{NONCE}}';
style-src 'self';
img-src 'self' data: blob:;
font-src 'self';
connect-src 'self' https://YOUR_PROJECT_REF.supabase.co wss://YOUR_PROJECT_REF.supabase.co;
frame-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
If you need more ready-to-use policy shapes, the examples on https://csp-examples.com can save time, especially when adapting CSP to specific frameworks.
What I would avoid
For Supabase Auth, I would avoid these unless you can justify them:
script-src 'self' 'unsafe-inline' 'unsafe-eval' https:
connect-src *
img-src https:
frame-src *
These make CSP look present while removing most of its value.
A good CSP for Supabase Auth is not huge. Most apps need only:
- your own origin
- your exact Supabase origin
- WebSocket access to that same origin
- a safe strategy for scripts
That’s it.
If you want the official behavior details for auth flows and client setup, check the Supabase documentation and your framework’s CSP support docs. That combination is usually enough to get a strict policy working without sacrificing login.