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-src for your APIs and map providers
  • worker-src 'self' blob:
  • child-src blob: for older browser compatibility
  • font-src 'self' data: https:
  • frame-src only if you embed something external
  • object-src 'none'
  • base-uri 'self'
  • frame-ancestors 'none' if you do not want embedding

The two big gotchas are:

  1. Workers
    deck.gl / loaders / rendering paths can rely on workers or blob-backed worker URLs.

  2. 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.com
  • https://events.mapbox.com
  • https://*.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:;

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.

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.