If you use Clerk for auth, Content Security Policy gets awkward fast.
Clerk is easy to drop into a React or Next.js app. CSP is not. The friction shows up the moment you try to lock down script-src, remove unsafe-inline, or support Clerk’s hosted flows, widgets, and frontend API calls without punching giant holes in your policy.
I’ve dealt with this in production, and the pattern is always the same: auth works fine until someone turns on a real CSP, then sign-in modals break, frontend API calls fail, or a wildcard gets added “temporarily” and never leaves.
Here’s the practical comparison guide: the main ways to handle CSP with Clerk, their pros and cons, and what I’d actually ship.
The core CSP problem with Clerk
Clerk usually needs some combination of:
script-srcfor Clerk-provided frontend scripts or dynamic importsconnect-srcfor Clerk API and session endpointsframe-srcif Clerk uses embedded flows or hosted componentsimg-srcandstyle-srcdepending on widgets and branding assets
The exact domains depend on your Clerk setup and environment. That means you should verify against Clerk’s official docs for current endpoints and your own network traces:
- Official docs:
https://clerk.com/docs
The hard part is not “how do I allow Clerk.” The hard part is “how do I allow Clerk without downgrading the rest of my CSP.”
A lot of teams start with a broad policy like this real-world header from headertest.com:
content-security-policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-ZjEzNDhjYmYtY2U4ZS00NTQ2LTg5NjAtMzk4OTMxZmRkNzc0' '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 already carrying analytics and consent tooling. Add Clerk carelessly and you end up with a policy nobody understands.
Option 1: Allow Clerk domains directly in a static CSP
This is the most common approach. You add Clerk domains to script-src, connect-src, img-src, maybe frame-src, and call it a day.
A rough shape looks like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.clerk.com https://clerk.example-frontend-api;
frame-src 'self' https://clerk.example-frontend-api;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
You’ll need to replace those Clerk endpoints with the actual domains from your tenant and docs.
Pros
- Easy to understand
- Works with most existing apps
- Fastest path for teams migrating to CSP
- Good fit if your auth UI is heavily Clerk-managed
Cons
- You often end up with environment-specific domain sprawl
- Wildcards are tempting, and usually overused
- Static policies drift as vendors change infrastructure
- Easy to accidentally allow too much in
connect-src
My take
This is acceptable if you keep it narrow and audited. I would not start with https://*.clerk.com unless Clerk’s docs explicitly require that breadth for your setup. Auth is already a sensitive path; broad host allowlists there are lazy engineering.
Option 2: Use nonces and strict-dynamic for scripts, plus narrow Clerk hosts
If your app already generates per-request nonces, this is usually the best balance.
You trust only scripts you nonce, and then use strict-dynamic so trusted scripts can load their own dependencies. This cuts down the number of host-based entries you need in script-src. You still need Clerk hosts in connect-src and possibly frame-src, but script policy gets much cleaner.
Example for a Next.js app setting a nonce:
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import crypto from 'node:crypto'
export function middleware(req: NextRequest) {
const nonce = crypto.randomUUID()
const res = NextResponse.next()
const csp = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self' 'unsafe-inline'`,
`img-src 'self' data: https:`,
`font-src 'self'`,
`connect-src 'self' https://api.clerk.com https://your-frontend-api.clerk.accounts.dev`,
`frame-src 'self' https://your-frontend-api.clerk.accounts.dev`,
`frame-ancestors 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
`object-src 'none'`,
].join('; ')
res.headers.set('Content-Security-Policy', csp)
res.headers.set('x-nonce', nonce)
return res
}
Then pass the nonce into your rendered script tags.
Pros
- Much stronger than host-only script allowlists
- Reduces script-src sprawl
- Better protection against inline script injection
- Usually the cleanest long-term approach
Cons
- More setup in SSR frameworks
- Easy to break hydration if nonce plumbing is inconsistent
- Third-party tools with inline styles/scripts still cause pain
- Some teams don’t fully understand how
strict-dynamicchanges source list behavior
My take
If I’m building a serious app with Clerk and modern SSR, this is what I want. Nonce-based CSP plus a very small set of Clerk hosts in connect-src and frame-src is far better than maintaining a giant static host list forever.
Option 3: Keep Clerk on dedicated auth routes with a looser CSP
This is the “contain the mess” strategy.
Instead of forcing one perfect CSP across the whole app, you serve a stricter policy for the main product and a slightly more permissive one only on routes like:
/sign-in/sign-up/sso-callback/user-profile
Example split:
const authPaths = ['/sign-in', '/sign-up', '/user-profile']
function buildCsp(isAuthRoute: boolean, nonce: string) {
const base = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
`style-src 'self'`,
`img-src 'self' data: https:`,
`font-src 'self'`,
`frame-ancestors 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
`object-src 'none'`,
]
if (isAuthRoute) {
base.push(
`style-src 'self' 'unsafe-inline'`,
`connect-src 'self' https://api.clerk.com https://your-frontend-api.clerk.accounts.dev`,
`frame-src 'self' https://your-frontend-api.clerk.accounts.dev`
)
} else {
base.push(`connect-src 'self'`)
base.push(`frame-src 'none'`)
}
return base.join('; ')
}
Pros
- Limits Clerk-related allowances to the routes that need them
- Easier to reason about blast radius
- Lets the rest of the app stay stricter
- Great for high-sensitivity dashboards or admin apps
Cons
- More policy logic to maintain
- Route matching mistakes can cause weird auth failures
- Shared layouts can accidentally pull auth dependencies into non-auth pages
- Developers need discipline around component boundaries
My take
I like this approach a lot. It’s not as elegant as one globally perfect CSP, but it’s practical. If Clerk UI only appears on auth surfaces, don’t give every route the same permissions.
Option 4: Use unsafe-inline or broad wildcards to “just make Clerk work”
Yes, people do this.
Something like:
Content-Security-Policy:
default-src 'self' https:;
script-src 'self' 'unsafe-inline' 'unsafe-eval' https: *.clerk.com;
style-src 'self' 'unsafe-inline' https:;
connect-src 'self' https: wss:;
frame-src 'self' https:;
Pros
- It works quickly
- Reduces support friction during rollout
- Useful as a temporary debugging step
Cons
- Weak CSP in practice
- Makes XSS impact much worse
- Hard to tighten later because nobody wants to break auth
- Wildcards hide what your app actually depends on
My take
Don’t ship this unless you’re in a short-lived emergency and already have a cleanup ticket scheduled. “Temporary” CSP exceptions age like milk.
The real tradeoff: hosted auth convenience vs CSP strictness
Clerk gives you a lot of convenience. Prebuilt auth UI, hosted flows, social login glue, session handling. The cost is that your CSP has to account for Clerk’s runtime behavior, not just your own code.
That means the best CSP for Clerk is usually not the absolute strictest possible CSP. It’s the strictest one that still matches how Clerk actually operates in your app.
I’d rank the options like this:
- Nonce +
strict-dynamic+ narrow Clerk hosts - Route-specific CSP for auth pages
- Static host allowlist everywhere
- Broad wildcard /
unsafe-inlinefallback
Practical recommendations
Here’s what I’d do on a real project:
- Start with
Content-Security-Policy-Report-Only - Capture violations during actual sign-in, sign-up, MFA, password reset, and social login flows
- Add only the Clerk sources you can prove are required
- Keep Clerk allowances mainly in
connect-srcandframe-src - Use nonces for scripts if your framework supports it cleanly
- Avoid
default-src https:style shortcuts - Don’t rely on wildcards when a concrete host works
- Separate auth-route policy from app-route policy if needed
If you want ready-made policy patterns, https://csp-examples.com is useful for comparing structures before tailoring one to Clerk.
A sane baseline policy shape
For many Clerk integrations, this is the shape I’d aim for:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.clerk.com https://your-frontend-api.clerk.accounts.dev;
frame-src 'self' https://your-frontend-api.clerk.accounts.dev;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
report-to default-endpoint;
Then tighten from there.
Not the other way around.
That’s the part teams get wrong. They start with a fantasy CSP, then keep adding exceptions until it’s unreadable. Start from a policy that matches your actual Clerk integration, measure it, and narrow it deliberately. That’s how you end up with a CSP that still works six months later.