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.jsserves 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 --staticat 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/fxYou'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.jsGCS / 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
onExposureadapter 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'