# Installing airgap.js: Synchronous vs Asynchronous (/docs/consent-management/configuration/installing-airgap/installing-airgap-js)

This guide gives you the exact setup for both ways to load `airgap.js`, with complete, paste-ready markup.

## The one rule [#the-one-rule]

`airgap.js` regulates network requests, cookies, and DOM insertions like a client-side firewall. It can only regulate what happens **after** it initializes.

<Callout type="info" title="Tracking before airgap loading">
  **Nothing may track before** **`airgap.js`** **is ready.**
</Callout>

There are exactly two correct ways to honor that rule:

1. **Synchronous:** load `airgap.js` as the very first script, so it is already regulating by the time anything else runs. You then add trackers normally.
2. **Asynchronous:** load `airgap.js` off the critical path and gate every tracker, so none start until `airgap.js` is ready.

### Which should you use? [#which-should-you-use]

<Callout type="info" title="Use of synchronous vs. asynchronous callout">
  **Use synchronous loading.** It is the simplest setup that is correct by default: airgap regulates everything after it, so you don't have to touch your tracker code. The one `<script>` in `<head>` blocks briefly while the bundle loads.

  **Choose asynchronous only if** you have a measured need to keep `airgap.js` off the critical rendering path (e.g. strict LCP budgets). The tradeoff: you become responsible for gating every tracker, and any that slips through fires unregulated.
</Callout>

|                                       | Synchronous (recommended)              | Asynchronous                            |
| ------------------------------------- | -------------------------------------- | --------------------------------------- |
| How airgap loads                      | Blocking `<script>`, first in `<head>` | Injected async after an inline stub     |
| Who guarantees "nothing tracks first" | airgap (it's first)                    | **You** gate every tracker              |
| Tracker code changes                  | None; add trackers normally            | Every tracker moved behind a ready gate |
| Best for                              | Almost everyone                        | Strict performance budgets, SPAs        |

## Option 1: Synchronous loading (recommended) [#option-1-synchronous-loading-recommended]

**How it works:** because `airgap.js` is the first script on the page, it is already intercepting requests before any tracker runs. You add analytics and marketing scripts exactly as their vendors document them, with no gating or changes on your side, and airgap regulates them for you.

### Step 1: Get your bundle ID [#step-1-get-your-bundle-id]

Copy it from [Developer Settings → Installation](https://app.transcend.io/consent-manager/developer-settings/installation). Your bundle URL looks like `https://transcend-cdn.com/cm/<your-bundle-id>/airgap.js`.

### Step 2: Load `airgap.js` as the first script in `<head>` [#step-2-load-airgapjs-as-the-first-script-in-head]

It must come **before** any script, pixel, or resource hint that could make a tracking request.

### Step 3: Add your tracking scripts after it [#step-3-add-your-tracking-scripts-after-it]

Place every tracker below the airgap tag; they load normally.

### Complete example [#complete-example]

Everything below the airgap tag is regulated automatically. The airgap tag is first in `<head>`, and the trackers (Amplitude and a marketing pixel) come after it, unmodified.

```html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />

    <!-- 1) Load airgap.js first, before all other scripts/trackers.
         data-cfasync="false" stops Cloudflare Rocket Loader (and similar
         optimizers) from deferring or reordering it. -->
    <script
      data-cfasync="false"
      src="https://transcend-cdn.com/cm/<your-bundle-id>/airgap.js"
    ></script>

    <!-- Everything below is regulated by airgap.js -->

    <!-- 2) Amplitude, load its library, then initialize. -->
    <script src="https://cdn.amplitude.com/libs/analytics-browser-2-min.js"></script>
    <script>
      window.amplitude.init("AMPLITUDE_API_KEY", { autocapture: true });
    </script>

    <!-- 3) Any other (non-Google) marketing pixel / tag. -->
    <script src="https://example-pixel.com/pixel.js"></script>

    <!-- Google tags (gtag.js / GTM)? See the Google Consent Mode section below. -->
  </head>
  <body>
    <!-- your page -->
  </body>
</html>
```

### Notes and gotchas (sync) [#notes-and-gotchas-sync]

- **Keep** **`data-cfasync="false"`.** Cloudflare Rocket Loader and similar tools will otherwise defer/reorder the tag and break the "first script" guarantee.
- **Put airgap above resource hints that fetch**, such as `<link rel="preload">`/`preconnect` for tracker hosts.
- **Using a tag manager (e.g. GTM)?** Put the `airgap.js` tag above the container snippet so the non-Google tags it injects are regulated at the network level. Don't rely on the tag manager itself to enforce consent; it's leaky. For Google tags, use Consent Mode (see the Google Consent Mode section below).
- **Server-rendered React, Next.js, or Hydrogen?** Render this exact tag as the first element of `<head>` on both server and client. If the tag only exists in server HTML, hydration can remove it and break initialization. If that's hard, use async loading (Part 2) or see the [React Snippets](../../reference/airgap/react-snippets.mdx) guide.

## Option 2: Asynchronous loading [#option-2-asynchronous-loading]

**How it works:** you load `airgap.js` off the critical path. Until it's ready, nothing is regulated, so you must hold back every tracker and start each one only once airgap signals ready.

**Recommended pattern:** a tiny inline pre-init stub (synchronous, so the ready queue exists immediately) that exposes a memoized `getAirgap()` promise. Every tracker initializes inside `await getAirgap()`, so airgap loads once and trackers start only when it's regulating.

### Step 1: Get your bundle ID [#step-1-get-your-bundle-id-1]

Same as above, from [Developer Settings → Installation](https://app.transcend.io/consent-manager/developer-settings/installation).

### Step 2: Add the stub + `getAirgap()` helper as the first script in `<head>` [#step-2-add-the-stub--getairgap-helper-as-the-first-script-in-head]

This inline block is synchronous and tiny. It defines the ready queue and injects `airgap.js` asynchronously. Put it first so the queue exists before anything else runs.

### Step 3: Load and initialize every tracker inside `getAirgap()` [#step-3-load-and-initialize-every-tracker-inside-getairgap]

Load each tracker's library and start it inside the ready gate, using a small `loadScript` helper, so even the library fetch waits until `airgap.js` is regulating. Nothing that touches the network may run outside the gate.

### Complete example [#complete-example-1]

```html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />

    <!-- 1) First: pre-init stub + getAirgap() helper (inline, synchronous, tiny).
         This defines the ready queue and injects airgap.js asynchronously. -->
    <script>
      // Get your bundle ID at
      // https://app.transcend.io/consent-manager/developer-settings/installation
      var BUNDLE_ID = '<your-bundle-id>';

      var airgapAPI;
      window.getAirgap = function () {
        return (airgapAPI =
          airgapAPI ||
          new Promise(function (resolve, reject) {
            // Stub the ready queue before airgap.js exists.
            if (!(self.airgap && self.airgap.ready)) {
              self.airgap = Object.assign(
                {
                  readyQueue: [],
                  ready: function (cb) { this.readyQueue.push(cb); },
                },
                self.airgap
              );
            }

            // Resolve once airgap.js has loaded and is ready.
            self.airgap.ready(function (airgap) { resolve(airgap); });

            // Inject airgap.js asynchronously.
            var s = document.createElement('script');
            s.addEventListener('error', reject);
            s.async = s.defer = true;
            // Optional load options, e.g.:
            // s.dataset.lazyLoadUi = 'on';
            s.src = 'https://transcend-cdn.com/cm/' + BUNDLE_ID + '/airgap.js';
            document.documentElement.appendChild(s);
          }));
      };
    </script>

    <!-- 2) Gate: after airgap.js is ready, load each tracker's script,
         then initialize it. Even the library fetch happens after airgap is
         regulating, so nothing touches the network too early. -->
    <script>
      // Inject a <script> element and resolve once it has loaded.
      function loadScript(src) {
        return new Promise((resolve, reject) => {
          const script = document.createElement('script');
          script.async = true;
          script.src = src;
          script.onload = () => resolve();
          script.onerror = reject;
          document.head.appendChild(script);
        });
      }

      (async () => {
        // 1. Wait until airgap.js is loaded and regulating.
        await getAirgap();

        // 2. Amplitude: load its library, then initialize.
        await loadScript("https://cdn.amplitude.com/libs/analytics-browser-2-min.js");
        window.amplitude.init("AMPLITUDE_API_KEY", { autocapture: true });

        // 3. Any other (non-Google) marketing pixel / tag.
        await loadScript("https://example-pixel.com/pixel.js");

        // Google tags (gtag.js / GTM)? See the Google Consent Mode section below.
      })();
    </script>
  </head>
  <body>
    <!-- your page -->
  </body>
</html>
```

### Notes and gotchas (async) [#notes-and-gotchas-async]

<Callout type="warn" title="Warning on trackers loading before airgap">
  **Beware:** you are now importing `airgap.js` asynchronously. Since it can only regulate traffic after it loads, **you are responsible for ensuring no trackers load before** **`airgap.js`.** The caveat applies only to the window before init; once airgap is ready, quarantine and replay handle later consent changes on the same page.
</Callout>

- **Load tracker libraries inside the gate, too.** Don't add ungated tracker `<script>` tags to `<head>`; use the `loadScript` helper so even the library fetch happens after `airgap.js` is regulating.
- **Extend, never overwrite** **`self.airgap`.** The stub above uses `Object.assign({…}, self.airgap)` for exactly this reason: clobbering it destroys any queue another script already registered.
- **React, Next.js, or Hydrogen:** load airgap once at app startup (a provider), gate trackers with a hook, and remember the async React pattern does not load airgap first. Use the [React Snippets for airgap](../../reference/airgap/react-snippets.mdx) guide.

## Google tags & Google Consent Mode [#google-tags--google-consent-mode]

**This is the same whether you load airgap synchronously or asynchronously.** Google tags (gtag.js / GTM) are a special case: don't gate them behind airgap. Google Consent Mode is a signal integration: airgap passes consent state to Google and Google's own tags self-regulate (in Advanced mode they load early and send cookieless pings before consent), so gating would suppress that.

Instead, set safe denied consent defaults synchronously, before `airgap.js` loads. Then load airgap (the sync tag from Part 1, or the async stub from Part 2) and let your Google tags run normally; airgap pushes the regime default and consent updates as they resolve. Your non-Google trackers still follow the mode you chose: after airgap in sync, inside the gate in async.

```html
<!-- FIRST in <head>: denied Consent Mode defaults (synchronous, before airgap.js) -->
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){ dataLayer.push(arguments); }
  gtag('set', 'developer_id.dODQ2Mj', true); // Transcend Consent Mode developer ID
  gtag('consent', 'default', {
    analytics_storage: 'denied',
    ad_storage: 'denied',
    ad_user_data: 'denied',
    ad_personalization: 'denied',
    functionality_storage: 'denied',
    personalization_storage: 'denied',
    security_storage: 'denied',
  });
</script>
<!-- then load airgap.js, and your Google tags load normally -->
```

The snippet above is for sites that load Google tags directly with gtag.js. With Google Tag Manager, keep your standard container snippet; the one thing to change is placement. Google tells you to paste the GTM container "as high in `<head>` as possible," but with Transcend the denied defaults and `airgap.js` go above the GTM container, so airgap regulates everything GTM injects. The GTM `<body>` `<noscript>` block is unchanged.

```html
<!doctype html>
<html>
  <head>
    <!-- 1) Denied Consent Mode defaults (synchronous, first) -->
    <script>
      window.dataLayer = window.dataLayer || [];
      function gtag(){ dataLayer.push(arguments); }
      gtag('set', 'developer_id.dODQ2Mj', true); // Transcend Consent Mode developer ID
      gtag('consent', 'default', {
        analytics_storage: 'denied',
        ad_storage: 'denied',
        ad_user_data: 'denied',
        ad_personalization: 'denied',
        functionality_storage: 'denied',
        personalization_storage: 'denied',
        security_storage: 'denied',
      });
    </script>

    <!-- 2) airgap.js, above the GTM container so it regulates everything GTM
         injects. (Loading airgap async? Use the Part 2 stub here instead.) -->
    <script
      data-cfasync="false"
      src="https://transcend-cdn.com/cm/<your-bundle-id>/airgap.js"
    ></script>

    <!-- 3) Your standard GTM container snippet, unchanged -->
    <!-- Google Tag Manager -->
    <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
    new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
    j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
    'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
    })(window,document,'script','dataLayer','GTM-XXXXXXX');</script>
    <!-- End Google Tag Manager -->
  </head>
  <body>
    <!-- Google Tag Manager (noscript), unchanged -->
    <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
      height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
    <!-- your page -->
  </body>
</html>
```

Prefer not to touch page code? Set the defaults inside GTM on the built-in **Consent Initialization – All Pages** trigger, or load airgap from within GTM using Transcend's GTM template. Basic vs Advanced mode, the purpose mapping, and the full GTM template are covered in the [Google Consent Mode guide](../consent-integrations/google-consent-mode.mdx).

## Verify your install (either approach) [#verify-your-install-either-approach]

1. **Network tab:** hard-reload and confirm no tracker request fires before `airgap.js`. In sync mode, `airgap.js` is the first request; in async mode, no tracker should hit the network until after it.
2. **`airgap.export()`:** run it in the console before and after granting consent on the same page. Blocked requests should appear while unconsented and drain after consent. (If they appear but never drain, you have an initialization/hydration problem.)
3. **Google Tag Assistant** (if using GCM): confirm you do _not_ see "A tag read consent state before a default was set."

See [Consent Management Debugging and Testing](../../testing-and-troubleshooting/debugging-and-testing.mdx) for more.

## Related docs [#related-docs]

- [Load airgap.js Asynchronously](./loading-asynchronously.mdx)
- [airgap.js Load Options Glossary](../../reference/airgap/load-options.mdx)
- [React Snippets for airgap](../../reference/airgap/react-snippets.mdx)
- [Google Consent Mode](../consent-integrations/google-consent-mode.mdx)
- [airgap.js API Reference](../../reference/airgap/api.mdx)
- [Consent Management Quickstart](../../quickstart.mdx)
