kepler.gl is one of those libraries that looks simple from the outside and then quietly pulls in a lot of browser features once you ship it: Web Workers, WebGL, map tiles, fonts, API calls, and often third-party basemaps.
That makes Content Security Policy trickier than a plain React app.
If you lock CSP down too early, kepler.gl usually breaks in non-obvious ways:
- blank map canvas
- workers failing to start
- tiles not loading
- icons or fonts disappearing
- map style JSON fetching but not rendering
This guide is the practical version: what to allow, what usually breaks, and copy-paste CSP examples you can start from.
If you want to sanity-check your final header, headertest.com is handy for quickly seeing what the browser actually receives.
What kepler.gl typically needs
kepler.gl itself is usually embedded inside a React app, and the CSP requirements depend on what mapping stack you use around it.
Most setups need some combination of:
script-src 'self'style-src 'self' 'unsafe-inline'img-src 'self' data: blob: https:connect-srcfor your APIs and map providersworker-src 'self' blob:child-src blob:for older browser compatibilityfont-src 'self' data: https:frame-srconly if you embed something externalobject-src 'none'base-uri 'self'frame-ancestors 'none'if you do not want embedding
The two big gotchas are:
-
Workers
deck.gl / loaders / rendering paths can rely on workers or blob-backed worker URLs. -
Map provider domains
Your actual allowlist changes a lot depending on Mapbox, CARTO, Google Maps, self-hosted tiles, or custom APIs.
Minimal baseline CSP for kepler.gl
Start here if you want a sane default for a self-hosted app with kepler.gl and external tile/data requests.
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob: https:;
font-src 'self' data: https:;
connect-src 'self' https:;
worker-src 'self' blob:;
child-src 'self' blob:;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
Why this baseline works
style-src 'unsafe-inline'is often necessary in real React UI stacks. I don’t love it, but kepler.gl apps commonly need it unless you’ve aggressively cleaned up styling behavior.img-src data: blob: https:avoids random breakage from generated images, markers, and remote map assets.connect-src https:is broad, but good for first-pass debugging. Tighten this later.worker-src blob:saves you from the classic “Refused to create a worker from blob:” error.
Production-friendly tighter CSP
Once you know your exact providers, replace broad schemes with explicit hosts.
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob: https://api.mapbox.com https://*.tiles.mapbox.com;
font-src 'self' data: https://api.mapbox.com;
connect-src 'self'
https://api.example.com
https://api.mapbox.com
https://events.mapbox.com
https://*.tiles.mapbox.com;
worker-src 'self' blob:;
child-src 'self' blob:;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
This is much closer to something I’d actually ship.
CSP for kepler.gl with Mapbox
A lot of kepler.gl deployments use Mapbox styles, fonts, sprites, and tiles. Here’s a practical header.
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:
https://api.mapbox.com
https://*.tiles.mapbox.com;
font-src 'self' data:
https://api.mapbox.com;
connect-src 'self'
https://api.example.com
https://api.mapbox.com
https://events.mapbox.com
https://*.tiles.mapbox.com;
worker-src 'self' blob:;
child-src 'self' blob:;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
Domains you may need for Mapbox
Depending on your exact integration:
https://api.mapbox.comhttps://events.mapbox.comhttps://*.tiles.mapbox.com
If you self-host styles or tiles, remove Mapbox and add your own domains instead.
CSP for kepler.gl with Google Maps tiles or APIs
Google integrations usually need a wider set of domains than people expect.
Content-Security-Policy:
default-src 'self';
script-src 'self' https://maps.googleapis.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
img-src 'self' data: blob:
https://maps.gstatic.com
https://maps.googleapis.com
https://*.googleapis.com
https://*.gstatic.com;
font-src 'self' data:
https://fonts.gstatic.com;
connect-src 'self'
https://api.example.com
https://maps.googleapis.com
https://*.googleapis.com;
worker-src 'self' blob:;
child-src 'self' blob:;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
Google’s hostnames tend to sprawl. Expect to tune this with report logs.
Express / Node.js example
If your kepler.gl app is served by Express, this is the fastest copy-paste setup.
import express from 'express';
const app = express();
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
[
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob: https:",
"font-src 'self' data: https:",
"connect-src 'self' https://api.example.com https://api.mapbox.com https://events.mapbox.com https://*.tiles.mapbox.com",
"worker-src 'self' blob:",
"child-src 'self' blob:",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
"frame-ancestors 'none'"
].join('; ')
);
next();
});
app.use(express.static('dist'));
app.listen(3000);
If you use Helmet:
import express from 'express';
import helmet from 'helmet';
const app = express();
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "blob:", "https:"],
fontSrc: ["'self'", "data:", "https:"],
connectSrc: [
"'self'",
"https://api.example.com",
"https://api.mapbox.com",
"https://events.mapbox.com",
"https://*.tiles.mapbox.com"
],
workerSrc: ["'self'", "blob:"],
childSrc: ["'self'", "blob:"],
objectSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
frameAncestors: ["'none'"]
}
}
})
);
app.use(express.static('dist'));
app.listen(3000);
Nginx example
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https:; font-src 'self' data: https:; connect-src 'self' https://api.example.com https://api.mapbox.com https://events.mapbox.com https://*.tiles.mapbox.com; worker-src 'self' blob:; child-src 'self' blob:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;
Report-Only first. Always.
For kepler.gl, I strongly recommend starting with Content-Security-Policy-Report-Only before enforcing.
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob: https:;
font-src 'self' data: https:;
connect-src 'self' https:;
worker-src 'self' blob:;
child-src 'self' blob:;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
report-uri /csp-report;
That gives you violation data without taking your map down in production.
If you want a deeper refresher on how directives like connect-src, worker-src, and frame-ancestors behave, csp-guide.com is a good reference.
Common kepler.gl CSP errors and fixes
1. Refused to connect to tile/style/data endpoint
Browser console usually says something like:
Refused to connect to 'https://api.mapbox.com/...' because it violates the following Content Security Policy directive: "connect-src 'self'"
Fix: add the host to connect-src.
connect-src 'self' https://api.mapbox.com https://*.tiles.mapbox.com;
2. Refused to create a worker from blob
Refused to create a worker from 'blob:...' because it violates the following Content Security Policy directive: "worker-src 'self'"
Fix:
worker-src 'self' blob:;
child-src 'self' blob:;
I still include child-src because older browser behavior and library edge cases can be annoying.
3. Refused to load inline styles
This one is common in UI-heavy React apps around kepler.gl controls.
Fix:
style-src 'self' 'unsafe-inline';
Could you replace it with hashes or nonces? Sometimes. Is it worth the pain for most kepler.gl dashboards? Usually not.
4. Map icons or sprites missing
Fix img-src and sometimes font-src.
img-src 'self' data: blob: https:;
font-src 'self' data: https:;
5. App works locally but fails behind analytics or consent tooling
This happens because your app CSP covers more than kepler.gl. Real deployments often need analytics and consent domains too.
For example, this is a real-world CSP shape from a production site:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-ZDZlMzFjYmYtODdjZi00ZDc4LTkwY2MtOTY5ODljOTY3NmQ2' '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 normal. Your kepler.gl policy is only one part of the whole app.
My recommended starting point
If you just want the version I’d use first for a kepler.gl app in production, here it is:
Content-Security-Policy:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob: https:;
font-src 'self' data: https:;
connect-src 'self'
https://api.example.com
https://api.mapbox.com
https://events.mapbox.com
https://*.tiles.mapbox.com;
worker-src 'self' blob:;
child-src 'self' blob:;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
Then tighten it with actual violation reports.
That’s the right mindset for kepler.gl CSP: start practical, keep workers alive, explicitly allow your map/data providers, and only get fancy once the map is stable.