How it works Examples Docs Pricing Blog WP Blocks Install free

Blog /

The CSP allowlist, explained: what your app can fetch and why

The first time you ship a DesignSetGo App that tries to load a font from Google Fonts or fetch from a third-party API, something specific will happen.

You will open the browser console and see:

Refused to connect to 'https://api.example.com/v1/things'
because it violates the following Content Security Policy directive:
"connect-src 'self' https://your-site.example".

That is the runtime working correctly. The app you shipped did not declare in its manifest that it would talk to api.example.com. The runtime read the manifest, generated a CSP header that allowlisted only what the manifest declared, and the browser blocked the call.

This post is the model. What CSP is, what the runtime does with it, and how to declare what your app legitimately needs without writing a CSP header by hand.

The audience is the developer who hits the console error for the first time and wonders if they did something wrong. They did not. The system is working.

What CSP is, in two paragraphs

Content Security Policy is a browser-enforced header that tells the browser which origins your page is allowed to load resources from. It is a defense in depth against cross-site scripting, third-party tracking injection, and code that the page author did not intend to run. A page with a strict CSP can declare “scripts only from this origin” and “fetch only from these specific APIs,” and the browser refuses anything outside that list.

Without CSP, a vulnerability in a single dependency can exfiltrate data to an attacker-controlled server. With CSP, the attacker can write the code but the browser refuses to send the request. CSP turns “any compromised dependency steals your data” into “the compromised dependency throws a console error and the data stays put.”

For DesignSetGo Apps, CSP is the enforcement mechanism for the permission manifest. The manifest is a declaration of intent. The CSP header is what makes the declaration binding.

What the runtime generates

When a DesignSetGo App loads, the runtime serves it inside an iframe with a CSP header derived from the manifest’s runtime.external_origins array. The header looks roughly like this:

Content-Security-Policy:
  default-src 'self';
  connect-src 'self' https://api.example.com https://fonts.googleapis.com;
  font-src 'self' https://fonts.gstatic.com;
  img-src 'self' data: https://images.example.com;
  script-src 'self';
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  frame-ancestors https://your-wordpress-site.example;

The exact directives vary by app, but the shape is always the same. 'self' is the iframe’s origin (the app’s own bundle). Everything else is what the manifest declared. The browser then refuses to load anything not in the list.

You do not write the header. You write the manifest. The runtime translates.

How you declare external origins

In dsgo-app.json:

{
  "runtime": {
    "sandbox": "strict",
    "external_origins": [
      "https://api.example.com",
      "https://fonts.googleapis.com",
      "https://fonts.gstatic.com"
    ]
  }
}

The list is per-origin, not per-URL. Listing https://api.example.com allows fetches to any path on that origin. The runtime does not let you allowlist https://api.example.com/v1/safe/* and block https://api.example.com/v2/dangerous/*; the origin is the granularity browsers enforce at the network layer.

At install time, the runtime takes that list and turns it into the install dialog’s “external connections” section:

This app will connect to:

  • api.example.com
  • fonts.googleapis.com
  • fonts.gstatic.com

The site owner sees each origin in plain English and approves (or denies) at install. If they deny one, the manifest is rejected; install fails. If they approve, the CSP includes those origins; the app can fetch.

The three classes of network call

In a DesignSetGo App, every outbound network call falls into one of three classes. Knowing which is which is the entire model.

Class 1: same-origin ('self'). Calls to your own bundle’s origin. Loading your own CSS, your own images, your own JavaScript. Always allowed. You do not declare anything for this.

Class 2: declared external. Calls to origins you listed in runtime.external_origins. Google Fonts, third-party APIs, image CDNs, embedded widgets you genuinely depend on. The user sees these at install time and approves them. The browser allows them.

Class 3: blocked. Calls to any origin not in either of the above. The browser refuses them. The console gets a CSP violation log. Your fetch’s .catch() block runs.

If you find your app trying to talk to an origin it should not be talking to, the runtime is doing exactly what it should. Either add the origin to the manifest (if it is legitimate) or remove the call (if your agent invented it).

The bridge is not a network call

This is the surprise. Most apps’ “I need data from the WordPress site” use case is not an external origin. It is a postMessage to the parent window, which is the bridge.

When your app calls dsgo.posts.list(), no network request leaves the iframe. The call goes to the parent window via postMessage, the parent talks to WordPress through internal APIs, the result comes back via postMessage. No fetch, no CSP impact.

This is why most apps need no external origins declared. They use the bridge for site data and have nothing to fetch externally. The empty array is the right default:

"runtime": { "sandbox": "strict", "external_origins": [] }

An app with external_origins: [] is the cleanest install dialog: “This app makes no external connections.” It is also the most common shape, because most apps’ data needs are satisfied by the bridge.

If your app does need an external origin, declare it explicitly. Do not stuff * in there to make CSP errors go away. The dialog will say “this app will connect to: anywhere” and your users will, correctly, decline.

What the agent needs to know

If you are working with Claude Code, Cursor, or another agent on a DesignSetGo App, the brief is:

“Before writing any fetch to a non-local origin, add the origin to runtime.external_origins in dsgo-app.json. If you cannot tell whether the origin is needed, do not add it. The bridge handles WordPress data with no network call; prefer it.”

The starter’s CLAUDE.md ships with this in the agent-context section. It is one of the five facts the five things Claude Code should know post calls out.

The pattern the agent tends to get wrong on the first prompt: it writes the fetch first, then waits for the CSP error to remind it to update the manifest. The brief reverses that order. Manifest first; fetch second; verify in the console third.

What changes between dev and production

Nothing. The CSP is the same in local development (with npx designsetgo apps dev) as it is in deployed production. This is intentional: the most common production-only surprise (the app worked locally and broke in production) is exactly what running the same CSP in both environments prevents.

If you see a CSP error in dev, you will see the same error in production. Fix it in dev, and production is fine.

Three common patterns

Loading a Google Font. Add https://fonts.googleapis.com and https://fonts.gstatic.com to external_origins. The first serves the CSS; the second serves the font files. Both are required.

Calling an analytics API from the app. Most analytics calls are fine via the bridge (dsgo.telemetry.event(...) if you have Pro), but if you need to call your own analytics directly, declare its origin. Note: a bare third-party analytics SDK like GA4 or Mixpanel is often more invasive than you want sandboxed apps to be. Consider telemetry-that-respects-the-user for the alternative.

Embedding a YouTube video. Add https://www.youtube.com and https://www.youtube-nocookie.com to external_origins. Use the iframe-embed URL with the noembed parameter. The video plays inside your app’s iframe, which is inside the runtime’s iframe, which is inside the WordPress page. CSP applies at each layer.

The shorter version

The CSP is the enforcement layer for the manifest. The manifest is the declaration of intent. The two together turn “any compromised dependency steals your data” into “any compromised dependency throws a console error and stops.”

Declare what you need. Do not declare what you do not need. The bridge handles WordPress data and is not an external origin.

If you see a CSP error, the system is working. Either add the origin or remove the call.

Further reading