Overview
This article records the June 2026 infrastructure baseline. Atlas Systems has since expanded beyond the ten-repository scope described here. Reusable workflows are now pinned to immutable atlas-infra revisions and enforce additional repository and runtime contracts, while atlas-notify routes a wider set of named signal classes. The incidents, constraints, and decisions below remain accurate to the period described. Current estate classification and policy live in atlas-infra, and later case studies document the subsequent control-plane work. The £0 figures refer to this bounded build, not the current operating cost of the wider estate.
Before this build, Atlas Systems had five Cloudflare Workers in production, each deployed manually by running npx wrangler deploy from a terminal. There was no record of when a deploy happened, no indication if it failed, and no consistency between repos. The GitHub profile showed pushes in three repos and nothing from the other seven. Two repos had tests that ran locally but nowhere else.
The goal was to close every one of those gaps without adding cost, complexity for complexity's sake, or infrastructure that needs its own maintenance burden. Everything here runs on free-tier Cloudflare and GitHub Actions. The constraint mattered: a system that costs money to observe costs more money when it breaks.
The design principle throughout was fan-in: build one authenticated endpoint that every other service reports to, and build one place to look when something goes wrong. That principle is what shaped the architecture from the first day, and it is the principle two later pieces of work built directly on top of.
Architecture
The full system runs across ten repos. Five are Cloudflare Workers sitting behind api.atlas-systems.uk; three are Cloudflare Pages static sites that auto-deploy from Git; one is a Python library with no deploy; one is a reference repo for CI/CD patterns.
The Workers share a single hostname via Cloudflare's route system. Each Worker claims a specific path prefix, and Cloudflare routes requests to the most specific matching pattern. atlas-notify owns /notify* and /* as a catch-all for the API index; github-pulse owns /pulse*; site-pulse owns /site-pulse*; deploy-watch owns /deploy-watch*. No Worker knows about the others; they compose into one API namespace without any shared state or coordination code.
The api.atlas-systems.uk subdomain points at a Cloudflare-proxied AAAA record set to 100::, the IPv6 discard prefix. Workers intercept requests before they ever reach an origin, so the record exists only to give the proxy something to resolve. The actual response always comes from a Worker function, not a server.
Discord acts as the observability layer. Seven channels cover the full lifecycle: push-log receives every commit across every repo via GitHub webhooks; deploy-log receives Cloudflare Pages outcomes; api-deploys receives Worker deploy results; ci-cd receives test results; weekly-digest runs on a Sunday cron; vault-backups receives the daily atlas-vault status report. Each channel has a single, well-defined source of truth.
The Event Bus
atlas-notify is a Cloudflare Worker that accepts authenticated POST requests and formats them into Discord embeds. It supports three inbound authentication dialects in a single endpoint: Bearer token for internal services, HMAC SHA-256 signature verification for GitHub webhooks, and header equality for Cloudflare's own notification system. The caller does not need to know which dialect the Worker expects; the Worker detects which one was used and validates accordingly.
This matters because native webhooks from GitHub and Cloudflare use fixed auth mechanisms that cannot be changed. Building the Worker to accept all three means those services plug in directly without adapter scripts or credential translation layers.
Outbound formatting is handled by a set of formatters keyed on the source field of the incoming payload. A push from GitHub produces a different embed shape from a Docker health event or a generic alert. Unknown but authenticated sources still get delivered as amber "unknown event" embeds rather than being dropped, on the basis that visibility is always more useful than silence.
The test suite runs inside Cloudflare's actual Workers runtime via @cloudflare/vitest-pool-workers, not a Node simulation. This matters specifically for the HMAC verification step, which calls crypto.subtle, a real Web Crypto API. A test that mocks crypto.subtle would pass regardless of whether the actual implementation was correct; running inside workerd means a green test is a genuine guarantee about production behaviour.
Observability Workers
github-pulse
github-pulse is a read-only proxy between the portfolio site and the GitHub API. It holds the authentication token server-side and caches responses in Cloudflare KV with a one-hour TTL. The site fetches one JSON document containing aggregate stats across the account, and that document is generated by at most one upstream burst of GitHub API calls per hour regardless of how many page views occur in that window.
The original implementation derived recent commit data from GitHub's public events feed. That feed has a documented limitation: it marks a push's commits as non-distinct when it judges them already visible from a recent prior push to the same branch. A day of rapid small commits through the GitHub web UI trips this consistently, silently producing an empty "no recent commits" reading even when commits exist. The fix was to stop reading from the events feed for this metric entirely and query each repo's actual Commits API endpoint directly, filtered by author to exclude CI bot commits.
site-pulse
site-pulse is the same backend-for-frontend pattern applied to Cloudflare's GraphQL Analytics API. The token stays server-side, the response caches for an hour, and the site reads one clean JSON document. The interesting constraint here is Cloudflare's free-tier query window: the httpRequestsAdaptiveGroups dataset only allows a 24-hour time range per query on the free plan. A 30-day history cannot be retrieved in one call.
The resolution was to scope the endpoint honestly to a 24-hour snapshot and accumulate daily snapshots via a cron trigger rather than pretend a wider window is available. A separate /weekly endpoint sums the stored daily entries, with a field showing how many days of data have actually been collected. The display label on the homepage reflects that count directly, so it says "(1d)" on day one and "(7d)" after a week, rather than silently presenting a partial number as a full weekly total.
The permission that works for this dataset is Zone: Analytics: Read. The similarly named Account: Account Analytics: Read permission does not grant access to zone-scoped query data despite what the general documentation implies. This distinction was confirmed through five failed token configurations before the correct combination was found and documented.
deploy-watch
deploy-watch runs on a five-minute cron and polls Cloudflare's Pages deployment API. It posts to the deploy-log Discord channel only when a deploy's terminal outcome (success, failure, or canceled) genuinely changes from the previously recorded one. A deduplication key in KV stores the last reported deploy ID and status; duplicate outcomes produce no Discord message.
It also always writes the latest known snapshot to a separate KV key regardless of outcome, including mid-build states. The homepage's Live Signal section reads from /deploy-watch/latest on page load to show the real last deploy time, commit SHA, and build status. This replaced a previous implementation that derived "last deploy" from github-pulse's cached commit data, which was both indirect and subject to the hour-long cache delay.
Phase I — Worker Route Claiming
Adding the API index endpoint at api.atlas-systems.uk/ produced a 522 timeout rather than the expected JSON response. atlas-notify was deployed and returning 200 on /notify. The bare root path was not.
Worker routes are scoped per URL pattern, not per hostname. The existing route only claimed /notify*. An unmatched request fell through to the AAAA discard record, which by design goes nowhere, producing a connection timeout on Cloudflare's side.
Adding a second, broader route api.atlas-systems.uk/* claimed the root path without displacing the existing specific route. Cloudflare evaluates the most specific matching pattern first, so both routes coexist correctly.
Two routes on one Worker: a specific pattern for the primary path and a wildcard for the hostname root. Both must exist in wrangler.toml; routes added only via the dashboard are silently deleted on the next wrangler deploy.
Phase II — Cloudflare Bot Protection Blocking CI
The weekly digest workflow fetched from api.atlas-systems.uk/site-pulse to include site visit data. The site-pulse endpoint responded correctly from a browser and from curl on a local machine. From GitHub Actions, the same request returned a 403.
Cloudflare's free-tier Bot Fight Mode issued a Managed Challenge to the request. A GitHub Actions runner presents as a bare curl user agent from a Microsoft Azure datacenter IP, which exactly matches the heuristic profile for automated traffic. The challenge page is HTML, not JSON, so the downstream jq parse failed silently and the weekly digest posted zero site visits every week.
The free-tier Bot Fight Mode cannot be selectively bypassed by WAF custom rules. The "Skip" action in custom rules does not reach this specific mechanism on the free plan. Turning Bot Fight Mode off entirely for the zone was the available option given the constraint of not upgrading the Cloudflare plan. The trade-off is acceptable for this site: all routes are read-only public APIs with no authentication surface to protect.
Bot Fight Mode disabled zone-wide. The decision is documented in the site-pulse README so the reason is recoverable when reviewing security settings later.
Phase III — The zone_name Trap
Four of the five Workers had their route registered with zone_name = "atlas-systems.uk" in wrangler.toml. The moment deploy.yml started running these as automated CI deploys, every one of them failed with "Could not find zone for atlas-systems.uk", despite working perfectly when deployed locally.
The scoped CI token cannot resolve a zone name to a zone ID, since that lookup requires account-level membership access the token deliberately does not have. The full mechanical breakdown of why a narrower token loses this specific capability is in the CI/CD piece; the short version is that the narrower the token, the less it can introspect.
The fix was the same one carried forward into every Worker built afterward: zone_id in place of zone_name, everywhere, permanently.
All five routes switched to literal zone_id values. This became a hard rule for the estate well before it had a name; the CI/CD rollout the following month made it official policy.
Phase IV — package-lock.json Absent from Worker Repos
npm ci, used in every deploy workflow for its reproducibility guarantee, requires a lock file that matches package.json exactly. Three Worker repos had package.json files that were edited through GitHub's web interface to add wrangler as a devDependency, but no corresponding npm install was ever run locally to generate the lock file. CI failed on first run for each of these repos.
GitHub's file editor has no knowledge of npm's dependency graph. Editing package.json through the web interface does not generate or update package-lock.json. npm ci refuses to install from a package.json when no corresponding lock file exists, by design.
Running npm install locally after any package.json change generates the correct lock file. The pattern now is: any package.json edit, wherever it is made, requires a follow-up local npm install and a committed lock file before the next CI run.
Lock files committed to all five Worker repos. The lesson applies permanently: npm ci and hand-edited package.json files are incompatible without a locally-generated lock.
CI/CD Automation
Before this build, every Worker deployed via a manual terminal command. There was no gate preventing broken code from shipping, no record of when a deploy happened, and no notification if it failed. The five Worker repos now have identical deploy.yml GitHub Actions workflows that trigger on every push to main.
The workflow shape is: install dependencies with npm ci; run tests if they exist (only atlas-notify currently has a test suite); run npx wrangler deploy with scoped CI credentials; post the outcome, pass or fail, to the api-deploys Discord channel. The atlas-notify workflow has three jobs: test, deploy (gated on test passing), and notify. The other four have two: deploy and notify. The difference reflects the current state of test coverage; the pattern is ready to accept a test job in any repo without restructuring the workflow. This per-repo shape is the version that exists today; it is not the final one. The next piece of work replaces these five individual deploy.yml files with two reusable workflows shared across the whole estate.
CI uses a dedicated CF_WORKERS_DEPLOY_TOKEN with Workers Scripts Edit, Account Settings Read, Workers KV Storage Edit, and Zone Workers Routes Edit (all zones). This token is separate from the Cloudflare Pages read token used by deploy-watch, and both are separate from the Cloudflare Analytics token used by site-pulse. Scoping tokens to the minimum required permission per use case means a compromised CI token cannot read deployment history, and a compromised analytics token cannot modify Workers.
All ten repos now have GitHub webhooks posting to atlas-notify, which formats them as push embeds in the push-log Discord channel. Previously only three repos were wired up. The other seven were added in a single pass; the webhook setup is identical for every repo, same payload URL, same content type, same secret, same event selection.
The weekly digest workflow runs on a Sunday cron and pulls commit counts, PR activity, new repos, and site visit data from real API endpoints rather than estimating from the GitHub events feed. The commit count fix in particular was material: querying each repo's /commits endpoint directly with an author filter returns ground truth; the events feed's distinct_size field was returning zero for rapid successive pushes to the same branch, making a busy week of work look like no activity.
Personal Tooling
atlas-vault is a personal streaming data backup vault: a Cloudflare Worker that accepts authenticated POST requests containing exported watch history and stores them in KV under dated keys. The design is deliberately narrow. The previous version of this tool was a generic CORS proxy that captured any response whose URL contained certain keywords, regardless of which user generated the request. The GDPR problem was structural, not configurational; the fix was a rewrite, not a patch.
The new version only stores what the owner explicitly posts to it. The trigger is a Tampermonkey browser script that runs when aether.cx loads, reads the relevant localStorage keys containing watch history and bookmarks, and POSTs them to the vault endpoint if the last backup was not made today. It runs silently in the background and requires no manual action after the initial setup.
A daily cron sends a Discord report into vault-backups summarising the backup state: total library size, number of titles tracked, the most recently watched title with watch percentage, the currently active show with episode count, and a random pick from the full library. The report is generated live from the stored JSON each time, not from static fields, so the numbers are always accurate to the last backup.
Outcomes
The system now documents its own operation. Every push appears in push-log. Every Worker deploy produces a coloured outcome embed in api-deploys within about a minute. Every test run posts to ci-cd. The live signal section on the homepage shows real frontend deploy data, real backend health checks, and real GitHub activity, all from live API endpoints. The status page at status.atlas-systems.uk checks all four Workers in the browser on page load and refreshes every 30 seconds.
The transferable principle is the difference between infrastructure that runs and infrastructure that reports. A system that works silently and a system that works visibly are functionally identical until something breaks; at that point, the visible one has the history you need and the silent one requires archaeology. Two pieces of work followed this one. The per-repo deploy.yml pattern built here got replaced within weeks by a single reusable workflow shared across the whole estate, documented in full in the CI/CD piece; and the Discord-only event stream this system produces eventually got a second home, a live dashboard on the Lab page that reads the same events without anyone needing to scroll through a channel history to find them.