Skip to content

Custom bridges

Need a vendor the CMP doesn’t ship an adapter for? Host a small bridge file yourself and declare it under bridges.custom. The CMP loads it and drives it exactly like a built-in bridge — including pushing the deny default before your vendor loader runs, and forwarding every later change. No forking, no onConsentChange glue.

Your file registers a factory with LightningCMP.defineBridge(id, factory). The factory returns a bridge object and is handed a small sdk so you reuse the CMP’s off-critical-path loader:

type ConsentBridge = {
id: string;
init(choices: ConsentChoices): void; // once, with the pre-load state
update(state: ConsentState): void; // on every later change
};
type BridgeFactory = (options: unknown, sdk: BridgeSdk) => ConsentBridge;
type BridgeSdk = {
// Injects a <script src> off the critical path (idle-time, async, deduped).
injectScriptDeferred(src: string, attrs?: Record<string, string>): void;
};
  • init runs once with the pre-load choices; update runs on every change.
  • Both are called fail-soft — a throwing bridge never breaks the banner or the page.
  • Only category booleans cross the boundary (choices.necessary / .preferences / .statistics / .marketing) — never visitor PII.
  1. Host a bridge file on your own origin. It registers a factory by id:

    // https://cdn.acme.io/lightning-bridge.js (a plain script you host)
    LightningCMP.defineBridge("acme", (options, sdk) => ({
    id: "acme",
    init(choices) {
    // 1. Push your vendor's deny default FIRST.
    window.acmeq = window.acmeq || [];
    if (!choices.statistics) window.acmeq.push(["anonymize"]);
    // 2. Then load it off the critical path.
    sdk.injectScriptDeferred("https://cdn.acme.io/acme.js", {
    "data-lightning-bridge": "acme",
    });
    },
    update(state) {
    window.acmeq.push(["anonymize", !state.choices.statistics]);
    },
    }));
  2. Declare it in init()id must match the id you passed to defineBridge:

    lightning("init", {
    bridges: {
    custom: [
    {
    id: "acme",
    src: "https://cdn.acme.io/lightning-bridge.js",
    options: { accountId: "A1" },
    integrity: "sha384-…", // recommended (SRI)
    },
    ],
    },
    });

    Registration and request rendezvous either way — it works whether your file loads before or after the CMP asks for the bridge.

  • Same guarantees as native bridgesinit once with the pre-load choices, update on every change, both fail-soft.
  • Off-critical-path loadingsdk.injectScriptDeferred injects at idle time with async, deduped per src, so your vendor never blocks first paint.
  • Your code, your responsibility. A custom bridge is loaded from your origin and runs on your site; you own its behaviour and its compliance. Pin it with integrity (SRI); the CMP adds crossorigin="anonymous" when you do.
  • Forward-only. Custom bridges attach just after first paint (they own their own vendor loader), so ordering-critical tags like Google should stay native, not custom.

The bridge contract is versioned. Check LightningCMP.BRIDGE_API_VERSION if you need to guard against a CMP build your bridge wasn’t written for:

if (LightningCMP.BRIDGE_API_VERSION < 1) {
// Bail out or degrade — this CMP predates the API your bridge needs.
}
  • Built-in bridges — the adapters the CMP ships.
  • Script blocker — for vendors with no consent-aware mode at all.
  • The full authoring contract lives in docs/BRIDGE_SPEC.md in the repo (category mapping, testing requirements, security & privacy checklist).