FabricFabricExperiments
Deployment

Self-host (static export)

Run Fabric Experiments on a marketing site with no manifest-worker — sign once, upload to S3 / R2 / GCS / any static host.

fx publish --static <dir> is the self-host path: signs the current manifest with your dev keypair, copies the tag-loader IIFE bundle, and writes both into a directory ready to upload to any static host. No control plane, no manifest-worker, no Cloudflare account required.

Want even less setup? The Fabric-operated CDN at https://cdn.fabric.pro/v/0/experiments.tag.global.js serves the same bundle — paste one <script> tag and ship. See the tag-loader guide for the snippet. Use this self-host path when you need to keep all JavaScript on your own origin (CSP-strict installs, regulated industries, supply-chain reviews).

Use this when:

  • You're a small team / one site, and the hosted SaaS / Cloudflare path is more infrastructure than you need.
  • You want experiments running on a marketing site today without provisioning anything.
  • You already have a CDN — point fx publish --static at it.

For governance-heavy installs (multi-tenant, audit-export, signed manifests with key rotation), use the Cloudflare deployment path instead.

Workflow

# 1. Author + apply locally to fx dev's in-process control plane.
fx validate experiments/
fx apply experiments/

# 2. Sign + export to a directory.
fx publish --static ./public/fx

You'll see:

Signed manifest v3 (keyId=dev-default, contentHash=…) written to .fx/manifest.json
Static export written to ./public/fx (manifest.json + experiments.tag.global.js)

./public/fx/ now contains:

public/fx/
  manifest.json                 # signed Ed25519 manifest
  experiments.tag.global.js     # tag-loader IIFE (~4.4 KB gz)
  experiments.tag.global.js.map # source map (optional)

Upload to your CDN

S3 / CloudFront

aws s3 sync ./public/fx s3://acme-experiments/ \
  --cache-control 'public, max-age=60'

Set a short cache TTL on manifest.json (the SDK fetches it on every page load) and a long TTL on the bundle (immutable per build).

Cloudflare R2 (no manifest-worker)

wrangler r2 object put experiments/manifest.json --file ./public/fx/manifest.json
wrangler r2 object put experiments/experiments.tag.global.js --file ./public/fx/experiments.tag.global.js

GCS / Azure / Vercel Edge / Netlify — all work; just upload the directory and serve as static assets with CORS allowing your site origin.

Wire the tag-loader into your site

<script>
  window.__FX__ = {
    manifestUrl: 'https://cdn.example.com/fx/manifest.json',
  };
</script>
<script src="https://cdn.example.com/fx/experiments.tag.global.js" async></script>

That's it. The loader auto-creates an anonymous subject id, fetches the manifest, applies any running variants' domOps, and pushes fx_exposure events to window.dataLayer for GTM.

See the tag-loader guide for the full __FX__ config surface (custom adapters, exclusion predicates, preview secrets, custom storage).

Trade-offs

  • ✅ No infrastructure to provision.
  • ✅ Same signed-manifest verification path the hosted version uses.
  • ✅ Integrates with any CDN.
  • ❌ No automatic key rotation — you re-publish manually.
  • ❌ No central audit log — exposures land wherever your onExposure adapter sends them, but Studio-style audit history isn't available.
  • ❌ Key material lives on the publishing machine (.fx/keys/) — back it up in a secret manager.
  • ❌ Manifest is publicly readable (it's a CDN URL). The same is true of the hosted manifest-worker path; the signature is what makes it trustworthy, not its secrecy.

Combining with hosted

You can mix: run the hosted control plane for governance + audit, but also fx publish --static to a CDN as a backup / latency optimization. The signed manifests are byte-identical, so SDKs can fetch from either.

Re-publishing

fx publish --static is idempotent and fast — wire it into CI:

# .github/workflows/deploy-experiments.yml
- run: fx validate experiments/
- run: fx apply experiments/
- run: fx publish --static ./public/fx
- run: aws s3 sync ./public/fx s3://acme-experiments/ --cache-control 'public, max-age=60'

On this page