Cloudflare Workers Static Assets
Cloudflare Workers Static Assets lets you deploy static files to Cloudflare's global edge network and serve them directly from a Worker. This gives your applications sub-50ms datafile fetches from anywhere in the world, with no origin server to maintain.
GitHub Actions builds the datafiles and wrangler deploy pushes them to Cloudflare on every merge to main. Pull requests only validate - nothing reaches Cloudflare until a change is approved and merged.
How it works#
┌─────────────────────────────────┐│ Messagevisor repository ││ YAML definitions + tests │└───────────────┬─────────────────┘ │ push to main ▼┌─────────────────────────────────┐│ GitHub Actions ││ lint → test → build → deploy │└───────────────┬─────────────────┘ │ wrangler deploy ▼┌─────────────────────────────────┐│ Cloudflare Workers ││ Static Assets (global edge) ││ /datafiles/messagevisor-*.json │└───────────────┬─────────────────┘ │ fetch on startup + poll ▼┌─────────────────────────────────┐│ Your application │└─────────────────────────────────┘Prerequisites#
- A Cloudflare account
wranglerinstalled in your project (npm install --save-dev wrangler)- A Cloudflare API token with Workers and Pages deployment permissions
Wrangler configuration#
Create a wrangler.toml at the repository root. The assets directory points to wherever Messagevisor writes its datafiles.
name = "messagevisor-datafiles"compatibility_date = "2024-01-01"main = "worker/index.js"[assets]directory = "./datafiles"binding = "ASSETS"The worker entry point is minimal - it just serves the static assets with appropriate headers:
export default { async fetch(request, env) { const response = await env.ASSETS.fetch(request); // Allow any origin to fetch datafiles const headers = new Headers(response.headers); headers.set("Access-Control-Allow-Origin", "*"); headers.set("Access-Control-Allow-Methods", "GET, OPTIONS"); // Cache for 5 minutes at the edge and in the browser headers.set("Cache-Control", "public, max-age=300, s-maxage=300"); return new Response(response.body, { status: response.status, headers, }); },};If your applications and the Worker are on the same Cloudflare zone or you have no CORS requirements, you can simplify further:
export default { async fetch(request, env) { return env.ASSETS.fetch(request); },};GitHub Actions workflow#
The workflow has two jobs: validate runs on every pull request and push, deploy runs only on merges to main.
name: Messagevisoron: pull_request: push: branches: [main]jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npx messagevisor lint - run: npx messagevisor test - run: npx messagevisor build --showSize deploy: runs-on: ubuntu-latest needs: validate if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npx messagevisor build - run: npx wrangler deploy env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}CLOUDFLARE_API_TOKEN is a repository secret set in GitHub → Settings → Secrets and variables → Actions.
Sets-based deployment#
For projects using sets, build and deploy only the set that is being promoted to production:
deploy: runs-on: ubuntu-latest needs: validate if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: npm ci - run: npx messagevisor build --set=production - run: npx wrangler deploy env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}If you build multiple sets, deploy them to separate Worker routes or use separate wrangler.toml files per environment.
Fetching datafiles at runtime#
Once deployed, your Worker serves datafiles at the Worker's URL. Fetch them from your application at startup:
import { createMessagevisor } from "@messagevisor/sdk";const datafile = await fetch( "https://messagevisor-datafiles.your-account.workers.dev/messagevisor-web-en-US.json").then((res) => res.json());const m = createMessagevisor({ datafile, locale: "en-US", context: { platform: "web" },});Or with a custom domain mapped to the Worker:
const datafile = await fetch( "https://translations.yoursite.com/messagevisor-web-en-US.json").then((res) => res.json());Custom domain#
To serve from a subdomain you own, add a route in wrangler.toml:
name = "messagevisor-datafiles"compatibility_date = "2024-01-01"main = "worker/index.js"routes = [ { pattern = "translations.yoursite.com/*", zone_name = "yoursite.com" }][assets]directory = "./datafiles"binding = "ASSETS"The zone must be on Cloudflare and the DNS record for translations.yoursite.com must point to Cloudflare.
Cache behavior#
Cloudflare caches responses at the edge automatically. With Cache-Control: public, max-age=300, s-maxage=300, each edge node caches the file for 5 minutes before re-validating with the origin.
This means a deployment reaches all edge nodes within 5 minutes of the first request to each node after the deploy.
If you need immediate global propagation after a deploy, add a cache purge step:
- run: npx wrangler deploy env: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} - name: Purge Cloudflare cache run: | curl -X POST "https://api.cloudflare.com/client/v4/zones/${{ secrets.CLOUDFLARE_ZONE_ID }}/purge_cache" \ -H "Authorization: Bearer ${{ secrets.CLOUDFLARE_API_TOKEN }}" \ -H "Content-Type: application/json" \ --data '{"purge_everything":true}'For most translation update use cases, 5-minute propagation is acceptable. Reserve cache purging for time-sensitive changes like compliance copy or campaign launches.
Checking which revision is live#
Add a revision endpoint to the Worker so your application can check whether it has the latest datafile without downloading the full payload:
export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname === "/revision") { const revision = await env.ASSETS.fetch( new Request(new URL("/.messagevisor/REVISION", request.url)) ); return new Response(revision.body, { headers: { "Content-Type": "text/plain", "Access-Control-Allow-Origin": "*", "Cache-Control": "no-store", }, }); } const response = await env.ASSETS.fetch(request); const headers = new Headers(response.headers); headers.set("Access-Control-Allow-Origin", "*"); headers.set("Cache-Control", "public, max-age=300, s-maxage=300"); return new Response(response.body, { status: response.status, headers }); },};Your application can poll /revision, compare it with the revision it last loaded, and only fetch a new full datafile when the revision has changed.

