Methodology

Cloudflare Workers Fanout Cookbook — wrangler config, secrets, Logpush wiring for the 5-platform fanout

Copy-pasteable recipe for the Cloudflare Worker that fans out server-side conversion events to Google Ads, Meta, Microsoft, TikTok, and GA4. wrangler.toml configuration, secrets management via wrangler secret put, Logpush wiring, the Worker entry point, and the deploy command.

Five ad platforms, five different envelopes, five different auth schemes — and one Worker that has to fire all of them in parallel without letting a single failure take down the rest. Here is the orchestration shell, end to end.

This is the edge-runtime fanout layer of the server-side conversion attribution stack — the shell that receives the canonical event from your CRM, verifies it, transforms it per platform, and fires all five in parallel. The per-platform envelopes live in the sibling spokes (Google Ads, Meta); this is the wrangler config, the secrets discipline, the Logpush observability, and the roughly 150-line Worker that holds it together. It ships in week 2 and runs free-tier for most service-business volumes.

wrangler.toml

The config declares identity, runtime, routes, and bindings:

name = "events-fanout"
main = "src/index.ts"
compatibility_date = "2026-05-15"
compatibility_flags = ["nodejs_compat"]
workers_dev = false

[[routes]]
pattern = "events.example.com/v1/*"
custom_domain = true

[vars]
# Non-secret IDs — recoverable from each platform's UI, fine to commit
META_PIXEL_ID = "1234567890123456"
GA4_MEASUREMENT_ID = "G-XXXXXXXXXX"
GOOGLE_ADS_CUSTOMER_ID = "1234567890"

[observability]
enabled = true

[[analytics_engine_datasets]]
binding = "FANOUT_METRICS"
dataset = "events_fanout_metrics"

Four choices earn naming: compatibility_date pins the runtime to a tested release (don't auto-track latest); the /v1/* route is versioned at the path so a v2 can ship in parallel without breaking the CRM webhook URL; [vars] holds non-secret IDs (pixel/customer IDs are recoverable from each platform's UI — committing them is fine; the actual secrets are separate); and [[analytics_engine_datasets]] declares the dataset the Worker writes per-platform success and latency to.

Secrets — wrangler secret put

The five platform credentials plus the CRM webhook signing secret go in via wrangler secret put, never the repo:

wrangler secret put GOOGLE_ADS_REFRESH_TOKEN
wrangler secret put GOOGLE_ADS_DEVELOPER_TOKEN
wrangler secret put META_CAPI_ACCESS_TOKEN
wrangler secret put MS_ADS_REFRESH_TOKEN
wrangler secret put TIKTOK_EVENTS_ACCESS_TOKEN
wrangler secret put GA4_API_SECRET
wrangler secret put CRM_WEBHOOK_SECRET   # verifies the inbound webhook signature

Three rules: scope by environment (production secrets are separate from staging; CI can deploy but never read them — only the runtime can); rotate annually at minimum (platform tokens don't expire predictably, and rotation surfaces drift before it becomes a production failure); and never log secrets (a 401 from Meta logs META_CAPI_AUTH_FAILED, not the request with the token).

Want this Worker scaffolded, secured, and deployed in week 2 of the cluster build? Talk to the team. →

The Worker entry point — the fanout pattern

About 150 lines covering five jobs: verify the webhook signature, parse the canonical event, dispatch to five platforms in parallel, aggregate the results, emit metrics.

import { Hono } from 'hono'
import { verifyWebhookSignature } from './lib/crypto'
import { canonicalToGoogle } from './transforms/google'
import { canonicalToMeta } from './transforms/meta'
// …microsoft, tiktok, ga4

const app = new Hono<{ Bindings: Env }>()

app.post('/v1/events', async (c) => {
  const rawBody = await c.req.text()
  const signature = c.req.header('x-crm-signature') ?? ''
  if (!await verifyWebhookSignature(rawBody, signature, c.env.CRM_WEBHOOK_SECRET)) {
    return c.json({ error: 'invalid_signature' }, 401)
  }

  const canonical = JSON.parse(rawBody)
  const startedAt = Date.now()

  // Fire all five in parallel — one failure must not cascade
  const results = await Promise.allSettled([
    fireGoogle(canonicalToGoogle(canonical), c.env),
    fireMeta(canonicalToMeta(canonical, c.env.META_PIXEL_ID, canonical.consent_state), c.env),
    fireMicrosoft(canonicalToMicrosoft(canonical), c.env),
    fireTikTok(canonicalToTikTok(canonical), c.env),
    fireGA4(canonicalToGA4(canonical), c.env),
  ])

  results.forEach((r, i) => {
    const platform = ['google', 'meta', 'microsoft', 'tiktok', 'ga4'][i]
    c.env.FANOUT_METRICS.writeDataPoint({
      blobs: [platform, r.status, canonical.event_id],
      doubles: [Date.now() - startedAt],
      indexes: [canonical.event_name],
    })
  })

  return c.json({ received: canonical.event_id })
})

export default app

Three choices earn naming: Promise.allSettled, not Promise.all — one platform failing must not cascade, so each result surfaces independently; synchronous metric emission — Analytics Engine's writeDataPoint is fire-and-forget at sub-millisecond cost, one point per fanout result for the dashboards to aggregate; and the versioned route/v1/events is the contract surface, so a /v2 can migrate in parallel.

Observability — Logpush

Logpush ships every request log to a sink of your choice (R2, Datadog, BigQuery). Two patterns matter: dual-destination — one cheap long-retention copy to R2 for archive, one real-time copy to your APM for on-call; and structured logging at the boundary — the Worker writes one JSON line per request with event_type, platform, status, latency_ms, dedupe_key, so the sink indexes the fields and you query by platform, status, or latency instead of grepping free text.

Deploy — one shot from CI

# .github/workflows/deploy.yml
on: { push: { branches: [main] } }
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '24' }
      - run: npm ci
      - run: npm test
      - uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          command: deploy

Scope the token to deploy-only (Workers Scripts: Edit, Workers Routes: Edit), never full account. The deploy takes 5–15 seconds; a green CI build means a live edge deploy worldwide inside that window. Rollback is one command: wrangler rollback.

Closing

The Worker that anchors the fanout is roughly 150 lines, ships in week 2 of the four-week build, and runs free-tier for most service-business volumes. Pinned compatibility date, secrets separated by environment, structured logging, versioned routes — that discipline survives every CRM change, every platform quirk, and every request to add a sixth platform later. None of it is novel; the novelty is refusing to deviate from the pattern. The boring stack that prints.

Ready to ship this Worker, secured and observable, in week 2 of the cluster build? Book a 30-minute call →

Share X LinkedIn
Build it yourself?

Get the kit, not just the theory.

We'll send the build checklist behind this post — and the next pillar when it ships. One email, no drip sequence. Unsubscribe in one click.

Want this built for you?

Book a discovery call. We'll walk your numbers.

20 minutes. Tell us what's broken, hear what we'd ship in the next 90 days. No pitch deck.