Machine room

The machine room

How wi24rd.com is built. A colophon, not marketing.

How wi24rd.com is built

For the technically minded: wi24rd.com runs on Cloudflare’s platform from DNS to database, without a single server anyone has to watch. The site doubles as an open demo of what can be built on Cloudflare Workers and the rest of the platform, with the engine freely available in the tingsvad.dk project.

Everything below is what the site actually uses. The engine is open source in the tingsvad.dk project on GitHub; the site’s own content, design and code are private. The subpages measure while you look at them.

The edge

Name, shields and access in front of everything else
DNS and domain

The name lives on Cloudflare’s name servers

The wi24rd.com zone answers from Cloudflare’s anycast DNS, so the lookup happens in the city nearest whoever asks. Traffic is proxied through Cloudflare: the Workers have no public IP address of their own that could be attacked around the edge. DNSSEC is signed in the same zone, with the DS record placed at the registrar, so a validating resolver can reject forged answers.

Network and security

HTTP/3, HSTS and CSP

Everything is served over HTTP/3 from Cloudflare’s network with HSTS, so a browser never tries an unencrypted connection. The Content-Security-Policy allows only the site’s own scripts; the fonts are served from the site itself, and the only outside address is Cloudflare’s own analytics beacon. The pages are static files, so CSP and HSTS are set by the middleware that streams the HTML (_headers only applies to answers straight from the files). No cookies for guests.

DDoS, WAF and analytics

A shield that is on before anyone asks

Cloudflare’s DDoS protection stands in front of all traffic with no setup, and the managed WAF ruleset stops known attack patterns before they reach the Workers. The defences show every hour what it stopped, and the crawlers which bots came by. Web Analytics counts visits with a small script from Cloudflare’s own domain, without cookies or fingerprinting. The site’s own visitor counter at the bottom of the page is a salted day hash in KV: the salt changes every day and is thrown away, so the number cannot be traced back to an IP address.

request.cf

The edge knows where it stands

Every request arrives with a cf object from Cloudflare: data centre, protocol, TLS version, measured round-trip time and much more. A small route in the site Worker, /api/kant, reads four of the fields and sends them back to you, so you can see which of Cloudflare’s data centres answered (your way here). IP, city, country and provider are in the object too; the site does not read them, and nothing is stored.

Workers Cache

Made once, read many times

The figures behind the defences, the crawlers, the speed, Denmark right now and the audit are the same for everyone, so they are cached in front of the Worker with Cache-Control: public, a Cache-Tag and an ETag derived from the content. The next guest gets the answer from the edge cache without the Worker running; afterwards it is revalidated with If-None-Match and an empty 304. When Cron has collected new figures, the Worker calls ctx.cache.invalidate({tags}). The x-wi24rd-kantcache header says whether the answer came from the cache.

Secrets Store

The keys in one cabinet

The site’s keys are secrets set with wrangler secret put and never appear in the repo; wrangler.toml holds only clearly marked placeholders for account and zone ids. The account-wide Secrets Store can take over key by key: the code asks the store first and falls back to the Worker secret.

Delivery

The site itself: static files and a Worker that only wakes up when needed
Workers Static Assets

A static site with no build step at deploy

The whole site is one Worker with the files as Static Assets. Every push to main becomes a new release on the global network via Workers Builds. HTML, CSS and JavaScript sit in the repo as they are served; the pages are generated by small Python scripts that live there too. Images, scripts and stylesheets are sent straight from the edge without waking the Worker, and they cost nothing.

run_worker_first

Server logic next to the files

A list of patterns decides when the Worker answers before the files: the HTML pages, /api/* and /admin/*, but not /assets/*. A small router with no dependencies answers /api/kant and /api/besoeg itself, forwards /api/maskine/* to the machine Worker and fetches the rest from the files through env.ASSETS. All of it happens in the same request, in the data centre closest to you.

HTMLRewriter

One navigation, no build step

The pages are ordinary HTML files with placeholders (data-tv-del). A middleware streams every page through HTMLRewriter and inserts the top bar, navigation and footer from /partials/, fills in the language switch from the page’s hreflang, sets the breadcrumbs from /assets/sider.json and marks the page you are on. A response header says which parts were inserted; the audit checks it every morning.

Content negotiation

Markdown for machines

If a client asks for Accept: text/markdown, or appends index.md to a page’s address, the middleware answers with the page’s content as Markdown: title, description, canonical address and language versions as YAML front matter, then the text without navigation, scripts and decoration, and the header x-markdown-tokens with an estimate of the length. It is done with a small parser with no dependencies. Browsers never send text/markdown and get HTML as always; the answer carries Vary: Accept, so no cache mixes the two.

The error board

Errors both kinds of guest can read

A page that does not exist gives people a 404 page in the right language with links to the site’s pages. Machines asking for application/problem+json get an RFC 9457 answer with type, title, status, detail, instance, retryable and suggestions; application/json gives the site’s own error contract, and text/markdown gives front matter with http_status and retryable. Everything under /api/ always answers JSON. If something fails inside the middleware, it answers 503 with Retry-After, never 502.

Compute

The measurements in a Worker with no address of its own
Workers + Service Binding

The machine Worker behind /api/maskine/

Everything that takes more than sending a file lives in a separate Worker. The site Worker calls it through a service binding, a direct call between two Workers that never touches the public internet. The machine Worker has no address of its own, and the public proxy only reaches its fetch(). Health, topology, the counting works and every measurement below come from there.

Durable Objects

One object, locked to the EU

The European archive cabinet is one Durable Object with its own SQLite storage, fetched with the EU jurisdiction, so it can only live in a data centre in the EU. Next to it stands the same class without a lock, so the difference can be seen. Every seal a guest puts in the cabinet becomes a row in the object’s own table (ctx.storage.sql) with time and data centre, nothing else.

Workflows + Browser Run

The audit, step by step

The audit is a Workflow that takes one page at a time every morning, with a pause in between: the Worker fetches the page and checks the HTTP status and the inserted parts, and a headless browser (Browser Run) opens it, counts the key elements, reads JavaScript errors and takes a screenshot into R2. Every step is idempotent and retried. If pages fail, the last step opens one issue on GitHub and closes it itself when everything passes again.

Cron Triggers

Every hour and every morning

A schedule in the machine Worker collects the figures for the defences, the crawlers and the door (GraphQL Analytics API), the speed (Web Analytics) and Denmark right now (Radar) once an hour and puts the summaries in KV. Once a day it starts the audit. Page views never trigger anything; guests only read what has already been collected.

Data

Where the site remembers, as little as possible
Workers KV

Today’s visitors and the summaries

A globally replicated key-value store holds today’s visitors as salted day hashes (the salt changes daily and is thrown away) that expire after two days, so no cookie is needed. The same namespace holds the finished summaries for the defences, the crawlers, the speed and Denmark right now.

D1

SQLite at the edge

The rows behind the defences and the crawlers (one day at a time, kept for 95 days), the audit’s results per page and the daily quotas for Browser Run and the seals live in a D1 database. The map shows which region and data centre the database answered from, and whether it was the primary copy.

R2

Screenshots with no egress fee

The audit’s screenshots are stored in an R2 bucket and served through the machine Worker on /api/maskine/revision/billede/*, so the bucket is never public. R2 does not charge for outbound traffic, so the images can be shown as often as you like.

Analytics Engine + Analytics SQL

The counting works

The middleware writes one data point per page and the machine Worker one per call to an Analytics Engine dataset. A data point is a kind, a name, a language, a data centre and two numbers: no IP, no cookie, no session. The write never blocks the answer. The Worker reads the figures back with the Analytics SQL binding without an API token (/api/maskine/taelle) and caches the answer for five minutes. The infrastructure map shows which data centres have served guests today.

Durable Objects · SQL

The seals in the cabinet

The archive cabinet’s Durable Object uses its SQLite storage: every seal is a row with time, date and data centre, and the figures on the page are a GROUP BY inside the object itself. Only time and place, never who.

Tooling and quality

What keeps the rest honest
GitHub Actions

Unit tests on every push

Workflows in .github/ run the unit tests in tests/ on every push and pull request: the router, the Markdown parser, the error board and the machine Worker’s small modules. The tests only read; they send nothing. The engine’s test runs are public in the tingsvad.dk project.

Workers Builds + Worker Previews

Every branch its own release

Workers Builds builds and deploys both Workers straight from GitHub. Every branch and pull request gets a Worker Preview at its own address with its own bindings, its own KV and its own machine Worker, so a test never writes into the site’s real figures. Only when the change is on main does it reach wi24rd.com.

Workers Issues + Traces

Errors in production in one place

Both Workers have Workers Issues and Traces turned on: runtime errors are collected and grouped by stack, and a share of the calls is traced all the way through the bindings, so you can see whether the time went in D1, R2 or the browser. Every answer also carries a Server-Timing header, which “Show the machinery” on the subpages reads.

Web Analytics

Measured with real guests

Cloudflare Web Analytics measures Core Web Vitals with the guests themselves, using a small script without cookies. The speed fetches the figures every hour and shows them page by page and country by country.

bump.sh

Cache busting with no build step

All stylesheets and scripts are fetched with ?v=<version>. A small shell script, bump.sh, changes the version in every HTML file before a push, so guests’ browsers and Cloudflare’s cache fetch the new files at once, while the unchanged ones stay in the cache.

Your way here

One small measurement, and a real one: where on Cloudflare’s network the page came from. It does not know who you are.

For the machines

Every page also exists as Markdown, and the site’s errors answer in a format a machine can read. Try it:

curl -H "Accept: text/markdown" https://wi24rd.com/en/machine-room/
curl https://wi24rd.com/en/about/index.md
curl -H "Accept: application/problem+json" https://wi24rd.com/does-not-exist/

The method is on the cards Markdown for machines and The error board above. An agent can also start with llms.txt.

The audit

Every morning every page in both languages is checked. Only the result is shown here; the table, the screenshots and the method are on the audit’s own page.

The engine on GitHub (tingsvad.dk) · MIT

Have a useful idea?

Feature requests, bug reports and questions are welcome. Every message gets a personal reply.