# How to Use Custom Domains on Cloudflare

<p>By default, the <a href="https://www.rudderstack.com/docs/sources/event-streams/sdks/rudderstack-javascript-sdk/" >JavaScript SDK</a> v3 talks to three RudderStack hosts. It loads from <code>cdn.rudderlabs.com</code>, fetches the source configuration from <code>api.rudderstack.com</code>, and sends events to your data plane URL. This guide moves all three behind a domain you control on Cloudflare.</p>
<p><a href="https://www.rudderstack.com/docs/user-guides/how-to-guides/custom-domains/" >Use Custom Domains</a> covers this setup for AWS CloudFront, with one distribution per host. On Cloudflare, one Worker serves all three hosts from one hostname by routing on the path. SDK paths start with <code>/v3/</code> or a version number such as <code>/3.23.0/</code>, and event paths start with <code>/v1/</code>, so they don&rsquo;t collide.</p>
<!-- end-chunk -->
<!-- begin-chunk -->
<h2 id="choose-an-approach">Choose an approach</h2><ul>
<li><strong>Option A: Proxy through your domain</strong>: A Cloudflare Worker sits in front of the three RudderStack hosts, and the site loads everything from your own hostname. The SDK files still come from RudderStack, so new SDK releases reach you without any file management. RudderStack recommends this route, mainly because it reduces the impact of ad blockers and makes cookies first-party.</li>
<li><strong>Option B: Self-host the files</strong>: You download the SDK, plugins, and device mode integrations and store them yourself, for example in an R2 bucket on a custom domain. RudderStack recommends this only for a zero-gap architecture, where no data goes out to any external service. You also take on upgrades. Source configuration and events still need the proxy from option A, because those are API calls rather than static files.</li>
</ul>

<!DOCTYPE html>
<html lang="en">
<blockquote class="tip">
  <div class="tip-quote">
    
    <div class="tip-text">If the goal is ad blocker resistance and first-party cookies, option A is enough. Choose option B only if you have a hard requirement to keep requests off <code>cdn.rudderlabs.com</code>.</div>
  </div>
</blockquote>
</html>
<p>Note the following:</p>
<ul>
<li>Ad blockers match RudderStack&rsquo;s host names, not the SDK&rsquo;s file names or paths.</li>
<li>AdGuard&rsquo;s Tracking Protection filter and the EasyPrivacy list, which uBlock Origin and Brave use by default, block <code>cdn.rudderlabs.com</code> when another site loads it, and block event requests such as <code>/v1/track</code> on <code>rudderstack.com</code> hosts. Those lists don&rsquo;t match <code>rsa.min.js</code>, <code>/v1/page</code>, or <code>/rsaRequest</code> on other hosts.</li>
<li>In tests with AdGuard in Safari, the page that used the default hosts loaded nothing, and the same page proxied through its own hostname loaded the SDK and delivered events. That result holds only when all three hosts go through the proxy.</li>
<li>Proxying the SDK files alone still loses tracked events.</li>
<li>The lists already name some individual data plane hosts and could add a proxy hostname the same way, so the proxy reduces blocking rather than preventing it.</li>
</ul>
<!-- end-chunk -->
<!-- begin-chunk -->
<h2 id="prepare-the-setup">Prepare the setup</h2><ul>
<li>Confirm the domain uses Cloudflare DNS, and that you can create Workers. For option B, confirm you can create R2 buckets.</li>
<li>Choose a hostname for the proxy, for example <code>rs.example.com</code>. If you plan to use server-side cookies, put it on the same registrable domain as the website.</li>
<li>In the RudderStack dashboard, copy the source <a href="https://www.rudderstack.com/docs/get-started/introduction/glossary/#write-key" >write key</a> and the data plane URL shown at the top of the <a href="https://www.rudderstack.com/docs/dashboard-guides/#connections" >Connections</a> page.</li>
<li>Note the SDK version. For option B, pin an exact version from the <a href="https://www.npmjs.com/package/@rudderstack/analytics-js?activeTab=versions" >npm versions page</a>.</li>
<li>Test on a staging site or hostname before production.</li>
<li>If the website sets a Content Security Policy, keep it available. You update it in <a href="#step-5-update-the-content-security-policy" >Step 5: Update the Content Security Policy</a>.</li>
<li>Confirm which sites and sources use this setup, and whether they share a registrable domain with the proxy hostname.</li>
<li>Confirm whether you install the SDK with the CDN snippet or with npm.</li>
<li>Confirm whether you need <a href="https://www.rudderstack.com/docs/sources/event-streams/sdks/rudderstack-javascript-sdk/server-side-cookies/" >server-side cookies</a>.</li>
<li>Confirm which device mode destinations are connected. You check their integration files in <a href="#verify-the-setup" >Verify the setup</a>.</li>
</ul>
<!-- end-chunk -->
<!-- begin-chunk -->
<h2 id="proxy-through-your-domain">Proxy through your domain</h2><p>Send all of the following through one hostname:</p>
<table>
  <thead>
      <tr>
          <th>Request</th>
          <th>Origin</th>
          <th>Requirements</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SDK files</td>
          <td><code>cdn.rudderlabs.com</code></td>
          <td>Core SDK, plugins, and device mode integrations. Proxy the whole host, not individual files. The SDK rewrites <code>/v3</code> in its paths to its current version, so both <code>/v3/modern/plugins/...</code> and <code>/3.x.y/modern/plugins/...</code> have to resolve.</td>
      </tr>
      <tr>
          <td>Source configuration</td>
          <td><code>api.rudderstack.com</code></td>
          <td>The SDK sends a <code>GET</code> request to <code>/sourceConfig</code> and sends the write key in the <code>Authorization</code> header. <code>Authorization</code> and <code>Origin</code> must reach RudderStack unchanged.</td>
      </tr>
      <tr>
          <td>Events</td>
          <td>Your data plane URL</td>
          <td>Paths start with <code>/v1/</code> (<code>track</code>, <code>page</code>, <code>identify</code>, and so on). Don&rsquo;t cache these requests. Forward <code>Origin</code> and <code>Content-Encoding</code>.</td>
      </tr>
      <tr>
          <td>Server-side cookies (optional)</td>
          <td>Data plane, same as events</td>
          <td>The <code>/rsaRequest</code> path. The data plane handles it.</td>
      </tr>
      <tr>
          <td>Error reports</td>
          <td>Data plane</td>
          <td>The <code>/rsaMetrics</code> path.</td>
      </tr>
  </tbody>
</table>
<!-- end-chunk -->
<!-- begin-chunk -->
<h3 id="step-1-create-the-worker">Step 1: Create the Worker</h3><p>In the Cloudflare dashboard, go to <strong>Workers &amp; Pages</strong> and create a Worker, or create one with Wrangler. Use the code below and set <code>DATA_PLANE_ORIGIN</code> to your data plane URL. Treat it as a starting point and test it on staging first.</p>
<div class="rs-code">
  <div class="rs-code__head">javascript<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-javascript" data-lang="javascript">// Cloudflare Worker: first-party proxy for the RudderStack JavaScript SDK (v3)
const CDN_ORIGIN = &#34;https://cdn.rudderlabs.com&#34;;
const CONFIG_ORIGIN = &#34;https://api.rudderstack.com&#34;;
const DATA_PLANE_ORIGIN = &#34;https://&lt;YOUR_DATA_PLANE_HOST&gt;&#34;; // Connections page in the RudderStack dashboard

export default {
  async fetch(request) {
    const url = new URL(request.url);
    let origin = DATA_PLANE_ORIGIN; // events, /rsaRequest
    let init = {};

    if (/^\/(v3|\d+\.\d+\.\d+)\//.test(url.pathname)) {
      origin = CDN_ORIGIN; // SDK, plugins, device mode integrations
      init = { cf: { cacheTtlByStatus: { &#34;200-299&#34;: 300, &#34;300-599&#34;: -1 } } }; // errors are never cached
    } else if (url.pathname.startsWith(&#34;/sourceConfig&#34;)) {
      origin = CONFIG_ORIGIN; // write key arrives in the Authorization header
    }

    const upstream = new Request(origin + url.pathname + url.search, request);
    upstream.headers.set(&#34;X-Forwarded-For&#34;, request.headers.get(&#34;CF-Connecting-IP&#34;));
    if (origin !== DATA_PLANE_ORIGIN) upstream.headers.delete(&#34;Cookie&#34;); // keep the site&#39;s cookies first-party
    return fetch(upstream, init);
  }
};</code></pre></div>
</div>
<p>SDK requests are cached for five minutes. When the custom domain isn&rsquo;t on CloudFront, set the maximum TTL to 300 seconds. See <a href="https://www.rudderstack.com/docs/user-guides/how-to-guides/custom-domains/serve-js-sdk/#ttl-settings" >TTL settings</a>. Error responses aren&rsquo;t cached, so a file that is briefly missing from the CDN doesn&rsquo;t stay broken for five minutes. Source configuration and events are never cached.</p>
<p>The Worker removes the browser&rsquo;s cookies from SDK and source configuration requests, so cookies the website sets for its whole domain, such as login cookies, aren&rsquo;t sent to RudderStack.</p>
<p>The data plane and the config API send their own CORS headers. The SDK files need CORS too. The modern build loads its plugins as JavaScript modules, which browsers fetch with CORS. The Worker passes on the <code>Access-Control-Allow-Origin: *</code> header that <code>cdn.rudderlabs.com</code> sends, so you don&rsquo;t add CORS headers in the Worker.</p>
<!-- end-chunk -->
<!-- begin-chunk -->
<h3 id="step-2-attach-a-custom-domain">Step 2: Attach a custom domain</h3><ol>
<li>Open the Worker, then go to <strong>Settings</strong> &gt; <strong>Domains &amp; Routes</strong> &gt; <strong>Add</strong> &gt; <strong>Custom Domain</strong>.</li>
<li>Enter the hostname, for example <code>rs.example.com</code>. It has to belong to a zone in your Cloudflare account. A custom domain sends every path on that hostname to the Worker and doesn&rsquo;t support wildcard DNS records.</li>
<li>Wait for the DNS record and certificate, then open <code>https://rs.example.com/v3/modern/rsa.min.js</code> in a browser. The SDK file loads.</li>
</ol>
<!-- end-chunk -->
<!-- begin-chunk -->
<h3 id="step-3-update-the-sdk-snippet">Step 3: Update the SDK snippet</h3><p>Copy the snippet from the <strong>Setup</strong> tab of the JavaScript source in the RudderStack dashboard and change three values: the SDK base URL, the data plane URL, and the config URL. See <a href="https://www.rudderstack.com/docs/sources/event-streams/sdks/rudderstack-javascript-sdk/installation/" >How to Install the JavaScript SDK</a> for the snippet, and <a href="https://www.rudderstack.com/docs/sources/event-streams/sdks/rudderstack-javascript-sdk/load-js-sdk/#loading-options" >Loading options</a> for each <code>load</code> option.</p>
<div class="rs-code">
  <div class="rs-code__head">javascript<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-javascript" data-lang="javascript">var sdkBaseUrl = &#34;https://rs.example.com&#34;;

rudderanalytics.load(&#34;&lt;WRITE_KEY&gt;&#34;, &#34;https://rs.example.com&#34;, {
  configUrl: &#34;https://rs.example.com&#34;
});</code></pre></div>
</div>
<p>The default snippet sets <code>sdkBaseUrl</code> to <code>https://cdn.rudderlabs.com</code>. The SDK adds <code>/sourceConfig</code> to <code>configUrl</code> on its own.</p>
<p>If you install the SDK from npm, or the files aren&rsquo;t at the default paths, also set <code>pluginsSDKBaseURL</code> and <code>destSDKBaseURL</code>:</p>
<div class="rs-code">
  <div class="rs-code__head">javascript<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-javascript" data-lang="javascript">rudderanalytics.load(&#34;&lt;WRITE_KEY&gt;&#34;, &#34;https://rs.example.com&#34;, {
  configUrl: &#34;https://rs.example.com&#34;,
  pluginsSDKBaseURL: &#34;https://rs.example.com/v3/modern/plugins&#34;,
  destSDKBaseURL: &#34;https://rs.example.com/v3/modern/js-integrations&#34;
});</code></pre></div>
</div>
<!-- end-chunk -->
<!-- begin-chunk -->
<h3 id="step-4-turn-on-server-side-cookies">Step 4: Turn on server-side cookies</h3><p>Server-side cookies keep the SDK&rsquo;s cookies from being shortened by Safari&rsquo;s tracking prevention. This step is optional. See <a href="https://www.rudderstack.com/docs/sources/event-streams/sdks/rudderstack-javascript-sdk/server-side-cookies/" >How to Use Server-side Cookies in JavaScript SDK v3</a>.</p>
<p>Turn them on with <code>useServerSideCookies: true</code> in the <code>load</code> options. The SDK then sends a <code>POST</code> request to <code>/rsaRequest</code>, and the Worker forwards it to the data plane. By default the request goes to the root of the website&rsquo;s domain, for example <code>https://example.com/rsaRequest</code> for a page on <code>www.example.com</code>.</p>
<p>To handle these requests, add a Worker route for <code>example.com/rsaRequest*</code> (the root domain has to be proxied through Cloudflare) or set <code>dataServiceEndpoint</code> to <code>https://rs.example.com/rsaRequest</code> (v3.34.0 or later).</p>

<!DOCTYPE html>
<html lang="en">
<blockquote class="tip">
  <div class="tip-quote">
    
    <div class="tip-text">Use the <code>dataServiceEndpoint</code> option. In tests, Safari kept these cookies for a year when the website and <code>rs.example.com</code> were both on Cloudflare. The 7-day Safari storage limit applies only when you use the <code>dataServiceEndpoint</code> option on a domain that isn&rsquo;t on Cloudflare.</div>
  </div>
</blockquote>
</html>
<ul>
<li>The proxy hostname has to resolve inside your own DNS zone. If it resolves through a CNAME to a different registrable domain, Safari limits these cookies to seven days.</li>
<li><code>Origin</code> must reach the data plane, otherwise the data plane rejects the requests. The Worker above forwards it.</li>
<li>One response carries several <code>Set-Cookie</code> headers. Anything that merges them drops all but one cookie without an error.</li>
<li>If the request fails, the SDK sets the cookies in the browser instead and logs an error in the console. Events still arrive, so check that <code>/rsaRequest</code> returns 200 in the <strong>Network</strong> tab rather than relying on events.</li>
</ul>
<p>After you deploy, check that each cookie comes back as its own <code>Set-Cookie</code> header. Replace <code>&lt;your_host&gt;</code> with <code>example.com</code> for the route option, or with <code>rs.example.com</code> for the <code>dataServiceEndpoint</code> option:</p>
<div class="rs-code">
  <div class="rs-code__head">bash<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-bash" data-lang="bash">curl -i -X POST &#39;https://&lt;your_host&gt;/rsaRequest&#39; \
  -H &#39;Content-Type: application/json&#39; \
  --data &#39;{
    &#34;reqType&#34;: &#34;setCookies&#34;,
    &#34;workspaceId&#34;: &#34;&lt;your_workspace_id&gt;&#34;,
    &#34;data&#34;: {
      &#34;options&#34;: { &#34;maxAge&#34;: 31536000000, &#34;path&#34;: &#34;/&#34;, &#34;sameSite&#34;: &#34;Lax&#34;, &#34;secure&#34;: true },
      &#34;cookies&#34;: [
        { &#34;name&#34;: &#34;rl_anonymous_id&#34;, &#34;value&#34;: &#34;test-anonymous-id&#34; },
        { &#34;name&#34;: &#34;rl_user_id&#34;, &#34;value&#34;: &#34;test-user-id&#34; }
      ]
    }
  }&#39;</code></pre></div>
</div>
<p>The response must contain two separate <code>Set-Cookie</code> headers. A single combined header, or only the last cookie, means something between the browser and the data plane merged or dropped them.</p>
<!-- end-chunk -->
<!-- begin-chunk -->
<h3 id="step-5-update-the-content-security-policy">Step 5: Update the Content Security Policy</h3><p>If the site sets a Content Security Policy, allow the new hostname in <code>script-src</code> and <code>connect-src</code>. Remove <code>cdn.rudderlabs.com</code>, <code>api.rudderstack.com</code>, and the old data plane host once nothing else uses them. Device mode destinations can still need their own vendor domains, so check the destination&rsquo;s page before you remove entries. If <code>script-src</code> doesn&rsquo;t allow <code>'unsafe-eval'</code>, the loading snippet&rsquo;s browser check fails and it loads the legacy build. The SDK still works, but the legacy build is larger.</p>
<!-- end-chunk -->
<!-- begin-chunk -->
<h2 id="self-host-the-files">Self-host the files</h2><p>Use these steps in place of the SDK file route in <a href="#proxy-through-your-domain" >Proxy through your domain</a>. Keep the Worker for source configuration and events. The SDK routes in the Worker go unused.</p>
<p><a href="https://www.rudderstack.com/docs/user-guides/how-to-guides/self-hosting-js-sdk/" >Self-host JavaScript SDK in Your CDN</a> describes the same directory layout. The Cloudflare steps below cover an R2 bucket.</p>
<!-- end-chunk -->
<!-- begin-chunk -->
<h3 id="set-the-directory-structure">Set the directory structure</h3><p>RudderStack recommends the layout used on its own CDN. There are two folders under a base directory:</p>
<ul>
<li><code>v3</code> holds the core SDK files <code>rsa.min.js</code> and <code>rsa.min.js.map</code>, in <code>modern</code> and <code>legacy</code> subfolders.</li>
<li><code>3.x.y</code>, named after the exact SDK version, holds the device mode integrations in <code>js-integrations</code> and the plugins in <code>plugins</code>. Plugins exist only for the modern bundle, because the legacy bundle has them built in.</li>
</ul>
<p>Keep the core file and the version folder on the same SDK version. The core file looks for its plugins and integrations in the folder named after its own version.</p>
<!-- end-chunk -->
<!-- begin-chunk -->
<h3 id="step-1-create-the-bucket-and-connect-a-domain">Step 1: Create the bucket and connect a domain</h3><ol>
<li>In Cloudflare, go to <strong>R2</strong> and create a bucket.</li>
<li>Open the bucket, then go to <strong>Settings</strong> &gt; <strong>Public access</strong> &gt; <strong>Custom Domains</strong> &gt; <strong>Connect Domain</strong>, and enter a hostname such as <code>sdk.example.com</code>. Cloudflare adds the DNS record.</li>
</ol>

<html lang="en">
<blockquote class="info">
  <div class="tip-quote">
    
    <div class="tip-text">Connecting a custom domain makes the bucket public, so keep only the SDK files in it.</div>
  </div>
</blockquote>

</html>
<ol start="3">
<li>Add a CORS policy before the site starts using the bucket. In the bucket&rsquo;s <strong>Settings</strong>, under <strong>CORS Policy</strong>, allow <code>GET</code> from the website&rsquo;s origin:</li>
</ol>
<div class="rs-code">
  <div class="rs-code__head">json<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-json" data-lang="json">[{&#34;AllowedOrigins&#34;: [&#34;https://www.example.com&#34;], &#34;AllowedMethods&#34;: [&#34;GET&#34;]}]</code></pre></div>
</div>

<html lang="en">
<blockquote class="info">
  <div class="tip-quote">
    
    <div class="tip-text">The modern build loads its plugins as JavaScript modules, which browsers fetch with CORS. Without this policy, no plugins load and the SDK sends no events. Files requested before the policy existed stay cached without the CORS header until you purge the cache.</div>
  </div>
</blockquote>

</html>
<!-- end-chunk -->
<!-- begin-chunk -->
<h3 id="step-2-download-the-files">Step 2: Download the files</h3><p>Pick the SDK version and use it in every URL. Replace <code>3.x.y</code> below:</p>
<div class="rs-code">
  <div class="rs-code__head"><button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-text" data-lang="text">https://cdn.rudderlabs.com/3.x.y/modern/rsa.min.js
https://cdn.rudderlabs.com/3.x.y/modern/rsa.min.js.map
https://cdn.rudderlabs.com/3.x.y/modern/js-integrations/list.html
https://cdn.rudderlabs.com/3.x.y/modern/plugins/list.html</code></pre></div>
</div>
<p>The <code>v3</code> alias always points at the latest release, so an explicit version keeps the core file and the folders matched. The two <code>list.html</code> pages list the integration and plugin files and their source maps. RudderStack recommends downloading all of them so troubleshooting stays possible. Repeat with <code>legacy</code> in place of <code>modern</code> if you need to support older browsers. The legacy bundle has no <code>plugins</code> folder. The modern <code>plugins</code> and <code>js-integrations</code> folders also each have an archive of all their files, <code>all_plugins.tar.gz</code> and <code>all_integration_sdks.tar.gz</code>, if you prefer one download per folder.</p>
<!-- end-chunk -->
<!-- begin-chunk -->
<h3 id="step-3-upload-the-files-to-the-bucket">Step 3: Upload the files to the bucket</h3><p>Upload with the dashboard, Wrangler, or any S3-compatible client, and keep this layout.</p>

<html lang="en">
<blockquote class="info">
  <div class="tip-quote">
    
    <div class="tip-text">Check that <code>.js</code> files are served with a JavaScript content type. Browsers refuse to run plugins served without one, and the SDK then sends no events. <code>wrangler r2 object put</code> sets no content type unless you pass <code>--content-type application/javascript</code>, so check a plugin URL with <code>curl -I</code> after uploading. Uploads through the R2 dashboard get <code>application/x-javascript</code>, which browsers accept.</div>
  </div>
</blockquote>

</html>
<div class="rs-code">
  <div class="rs-code__head"><button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-text" data-lang="text">&lt;base&gt;/v3/modern/rsa.min.js
&lt;base&gt;/v3/modern/rsa.min.js.map
&lt;base&gt;/3.x.y/modern/js-integrations/&lt;destination&gt;.min.js
&lt;base&gt;/3.x.y/modern/plugins/&lt;files from plugins/list.html&gt;</code></pre></div>
</div>
<!-- end-chunk -->
<!-- begin-chunk -->
<h3 id="step-4-update-the-loading-snippet">Step 4: Update the loading snippet</h3><p>Set <code>sdkBaseUrl</code> to your base path. The snippet adds <code>/v3/</code> itself, so leave it out:</p>
<div class="rs-code">
  <div class="rs-code__head">javascript<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-javascript" data-lang="javascript">var sdkBaseUrl = &#34;https://sdk.example.com/&lt;base-directory-path&gt;&#34;;</code></pre></div>
</div>
<p>If the core file isn&rsquo;t called <code>rsa.min.js</code>, or its folder isn&rsquo;t <code>v3</code>, also set <code>sdkFileName</code> and <code>sdkVersion</code>:</p>
<div class="rs-code">
  <div class="rs-code__head">javascript<button class="rs-code__copy" type="button">
      
      Copy
    </button>
  </div>
  <div class="highlight"><pre class="chroma"><code class="language-javascript" data-lang="javascript">var sdkBaseUrl = &#34;https://sdk.example.com/rs-js-sdk-artifacts&#34;;
var sdkVersion = &#34;custom-version-id&#34;;
var sdkFileName = &#34;customsdk.min.js&#34;;</code></pre></div>
</div>
<p>With the recommended structure, the SDK finds integrations and plugins on its own. Otherwise, point to them with <code>destSDKBaseURL</code> and <code>pluginsSDKBaseURL</code>, as in <a href="#step-3-update-the-sdk-snippet" >Step 3: Update the SDK snippet</a>. Set <code>configUrl</code> and the data plane URL to the Worker hostname in the same way.</p>
<!-- end-chunk -->
<!-- begin-chunk -->
<h3 id="review-requests-that-still-leave-your-domain">Review requests that still leave your domain</h3><p>Two defaults can still reach other hosts. That matters if avoiding them is the reason you chose option B.</p>
<ul>
<li><strong>Polyfills:</strong> <code>polyfillIfRequired</code> defaults to <code>true</code>, and <code>polyfillURL</code> defaults to a <code>polyfill-fastly.io</code> address. The loading snippet also loads a script from <code>polyfill-fastly.io</code> on old browsers. Change that URL in the snippet too. Set <code>polyfillIfRequired</code> to <code>false</code>, or point <code>polyfillURL</code> at a copy you host. Whether you can drop polyfills depends on the browsers you support. See <a href="https://www.rudderstack.com/docs/sources/event-streams/sdks/rudderstack-javascript-sdk/load-js-sdk/#loading-options" >Loading options</a>.</li>
<li><strong>Device mode destinations:</strong> A destination can load a vendor script from the vendor&rsquo;s domain. The destination&rsquo;s page names any domain to allowlist.</li>
</ul>
<!-- end-chunk -->
<!-- begin-chunk -->
<h3 id="upgrade-the-sdk">Upgrade the SDK</h3><p>Nothing updates on its own with option B. To move to a new version, download that version&rsquo;s files (<a href="#step-2-download-the-files" >Step 2: Download the files</a>) into a new <code>3.x.y</code> folder and replace the core file under <code>v3</code>.</p>

<html lang="en">
<blockquote class="info">
  <div class="tip-quote">
    
    <div class="tip-text">Keep the previous version folder until you confirm the new one works, and for at least the browser cache time on the bucket&rsquo;s domain (four hours by default). Browsers keep the old core file until it expires.</div>
  </div>
</blockquote>

</html>
<!-- end-chunk -->
<!-- begin-chunk -->
<h2 id="verify-the-setup">Verify the setup</h2><ol>
<li>Open the site with the browser&rsquo;s <strong>Network</strong> tab open. The SDK file, the <code>/sourceConfig</code> request, and the <code>track</code> and <code>page</code> calls should all go to your hostname. Nothing should go to <code>cdn.rudderlabs.com</code>, <code>api.rudderstack.com</code>, or the old data plane URL.</li>
<li>Install the <a href="https://www.rudderstack.com/docs/sources/event-streams/sdks/rudderstack-javascript-sdk/events-tracking-assistant/" >RudderStack Assistant</a> browser extension. Its <a href="https://www.rudderstack.com/docs/sources/event-streams/sdks/rudderstack-javascript-sdk/events-tracking-assistant/interface-reference/#custom-domains" >Custom domains</a> section shows whether the data plane URL and plugins base URL are standard or custom.</li>
<li>Send a test event and confirm it appears in the source&rsquo;s live events in the RudderStack dashboard. See <a href="https://www.rudderstack.com/docs/user-guides/how-to-guides/debugging-destination-live-events/" >Debug Destination Live Events</a>. Check <code>request_ip</code> on the test event. It must show the client IP, not a Cloudflare IP.</li>
<li>Test with an ad blocker turned on, and in Safari. These are the main reasons for the setup.</li>
<li>Confirm that the integration file for each connected device mode destination returns a 200 response.</li>
<li>If server-side cookies are on, run the <code>curl</code> check from <a href="#step-4-turn-on-server-side-cookies" >Step 4: Turn on server-side cookies</a>.</li>
</ol>
<!-- end-chunk -->
<!-- begin-chunk -->
<h2 id="troubleshooting">Troubleshooting</h2><table>
  <thead>
      <tr>
          <th>Issue</th>
          <th><div>Solution</div></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CORS errors</td>
          <td>These usually come from a misconfigured CDN, not an SDK defect. See the <a href="https://www.rudderstack.com/docs/user-guides/how-to-guides/custom-domains/#faq" >FAQ</a> on <a href="https://www.rudderstack.com/docs/user-guides/how-to-guides/custom-domains/" >Use Custom Domains</a>. Confirm that <code>OPTIONS</code> requests are answered, <code>Origin</code> is forwarded, and CORS headers are present on source configuration and event responses.</td>
      </tr>
      <tr>
          <td>404 on a plugin or integration file</td>
          <td>Only some paths are proxied, or the version folder is missing (option B). The failing URL contains the exact SDK version, so compare it with what you stored.</td>
      </tr>
      <tr>
          <td>Option B loads no plugins and sends no events</td>
          <td>The console shows a CORS or MIME type error on a plugin file. Add the bucket&rsquo;s CORS policy (<a href="#step-1-create-the-bucket-and-connect-a-domain" >Step 1: Create the bucket and connect a domain</a>) and serve <code>.js</code> files with a JavaScript content type (<a href="#step-3-upload-the-files-to-the-bucket" >Step 3: Upload the files to the bucket</a>).</td>
      </tr>
      <tr>
          <td>Source configuration fails to load</td>
          <td>Check that the <code>Authorization</code> header reaches <code>api.rudderstack.com</code>. The write key travels in it.</td>
      </tr>
      <tr>
          <td>No events arrive</td>
          <td>Confirm the data plane URL in the Worker matches the dashboard and that event requests aren&rsquo;t cached.</td>
      </tr>
      <tr>
          <td>An old SDK version keeps loading</td>
          <td>SDK responses are cached for up to five minutes in this setup. Purge the cache in Cloudflare if you need the change sooner.</td>
      </tr>
      <tr>
          <td><code>RudderEncrypt</code> parse error in Safari</td>
          <td>Upgrade to the latest v3. Proxying the SDK alone doesn&rsquo;t fix it. If it continues on Safari or iOS, proxy all three hosts and see the <a href="https://www.rudderstack.com/docs/sources/event-streams/sdks/rudderstack-javascript-sdk/faq/" >JavaScript SDK FAQ</a>.</td>
      </tr>
      <tr>
          <td>Server-side cookies aren&rsquo;t set</td>
          <td>Look for a missing <code>Origin</code> header, merged <code>Set-Cookie</code> headers, a cached response, a root domain that isn&rsquo;t proxied through Cloudflare (route option), or a hostname that resolves through a CNAME on another domain.</td>
      </tr>
  </tbody>
</table>
<!-- end-chunk -->
<!-- begin-chunk -->
<h2 id="see-more">See more</h2><p><strong>RudderStack documentation</strong></p>
<ul>
<li><a href="https://www.rudderstack.com/docs/user-guides/how-to-guides/self-hosting-js-sdk/" >Self-host JavaScript SDK in Your CDN</a>: file layout, download URLs, and loading snippet changes.</li>
<li><a href="https://www.rudderstack.com/docs/user-guides/how-to-guides/custom-domains/" >Use Custom Domains</a>: the three hosts to proxy and the overall setup.</li>
<li><a href="https://www.rudderstack.com/docs/user-guides/how-to-guides/custom-domains/serve-js-sdk/" >Serve JavaScript SDK on Custom Domains</a>: SDK distribution settings and snippet changes.</li>
<li><a href="https://www.rudderstack.com/docs/user-guides/how-to-guides/custom-domains/fetch-source-config/" >Serve Source Configuration on Custom Domains</a>: headers and the <code>configUrl</code> option.</li>
<li><a href="https://www.rudderstack.com/docs/user-guides/how-to-guides/custom-domains/send-events/" >Send Events via Custom Domains</a>: proxying the data plane.</li>
<li><a href="https://www.rudderstack.com/docs/sources/event-streams/sdks/rudderstack-javascript-sdk/load-js-sdk/#loading-options" >Load JavaScript SDK</a>: every <code>load</code> option, including <code>destSDKBaseURL</code>, <code>pluginsSDKBaseURL</code>, <code>configUrl</code>, and the polyfill options.</li>
<li><a href="https://www.rudderstack.com/docs/sources/event-streams/sdks/rudderstack-javascript-sdk/installation/" >How to Install the JavaScript SDK</a>: CDN snippet and npm installation.</li>
<li><a href="https://www.rudderstack.com/docs/sources/event-streams/sdks/rudderstack-javascript-sdk/server-side-cookies/" >How to Use Server-side Cookies in JavaScript SDK v3</a>: <code>/rsaRequest</code>, proxy requirements, and the verification command.</li>
<li><a href="https://www.rudderstack.com/docs/sources/event-streams/sdks/rudderstack-javascript-sdk/faq/" >JavaScript SDK FAQ</a>: verifying traffic and the Safari storage error.</li>
<li><a href="https://www.rudderstack.com/docs/sources/event-streams/sdks/rudderstack-javascript-sdk/events-tracking-assistant/" >RudderStack Assistant</a>: debugging events in the browser.</li>
<li><a href="https://www.rudderstack.com/docs/user-guides/how-to-guides/debugging-destination-live-events/" >Debug Destination Live Events</a>: checking that events arrive.</li>
<li><a href="https://www.npmjs.com/package/@rudderstack/analytics-js?activeTab=versions" >SDK versions on npm</a>: the version list to pin from.</li>
<li><a href="https://github.com/rudderlabs/rudder-sdk-js" >RudderStack JavaScript SDK on GitHub</a>: source and release history.</li>
</ul>
<p><strong>Cloudflare documentation</strong></p>
<ul>
<li><a href="https://developers.cloudflare.com/workers/configuration/routing" >Workers routes and domains</a>: how Custom Domains and Routes differ.</li>
<li><a href="https://developers.cloudflare.com/workers/configuration/routing/custom-domains/" >Workers Custom Domains</a>: attaching a hostname to a Worker.</li>
<li><a href="https://developers.cloudflare.com/r2/buckets/public-buckets/" >R2 public buckets</a>: connecting a custom domain to a bucket.</li>
<li><a href="https://developers.cloudflare.com/cache/interaction-cloudflare-products/r2/" >Cache with R2</a>: serving bucket files through Cloudflare&rsquo;s cache.</li>
</ul>

