Skip to content

Platform engineering

Cycle Kirklees platform

Built and run the public site, signup gateway, and domain email for Cycle Kirklees, a volunteer-run cycling campaign group, on a self-hosted, £0-cost infrastructure budget.

Cyclists riding together on a Cycle Kirklees community ride

Cycle Kirklees is a volunteer-run cycling campaign group, and I'm one of those volunteers. Read more at cyclekirklees.org.uk.

When I joined, the group was running things off a tired old WordPress site, paying for managed hosting they really couldn't afford on a volunteer budget. Newsletters went out by hand from a personal Gmail account, and new members were onboarded by whoever on the committee had time to chase WhatsApp invites that evening. Naturally, the committee had a long wish list: a site they could update themselves, signups and newsletters that didn't hinge on someone's free time, and some way to actually know whether any of it was working. The one rule that shaped everything was that there was no operating budget to spend, so self-hosting wasn't really a choice so much as the only way any of this could happen at all.

That constraint runs through every decision that followed. The site itself is built on a vendored Sveltia CMS with GitLab OAuth, which gives committee members a simple git-based workflow where editing content just publishes it, with no per-seat cost and nothing tying them to a third-party platform. Everywhere else, I tried to follow the same instinct: solve the problem with infrastructure I already owned, and lean on services with a genuinely free tier rather than a trial that quietly turns into a bill a year later.

The platform thinking

What makes this a platform engineering project rather than a web build is that the committee members never need to think about infrastructure. The Sveltia CMS gives them a git-based editorial workflow: they edit content in a browser, hit publish, and the CI/CD pipeline handles the rest. That's a golden path: a self-service workflow where the cognitive load of deployment, hosting, and observability sits on the platform, not on the person publishing a blog post about a forthcoming consultation.

This is the same pattern platform engineering teams apply inside organisations: an Internal Developer Platform that lets product teams ship without understanding the underlying infrastructure. The difference is that most IDP projects have budget and a dedicated team. This one had neither, which made the constraint the design: every layer had to be self-sustaining without an on-call rotation or a procurement cycle.

How it fits together

Rather than build one big monolith, I split the platform into three pieces that each do one job well:

  • The public site (cyclekirklees) is an Eleventy, Tailwind, and Webpack static build. It serves the content, the blog, and the CMS-driven editorial that committee members maintain themselves through Sveltia.
  • The signup gateway (cyclekirklees-gateway) is an Express/TypeScript service that handles everything dynamic: member signup, newsletter double opt-in, and contact form submissions.
  • Domain email is self-hosted and runs alongside the rest of the stack, so cyclekirklees.org.uk addresses, inbound mail, and webmail no longer depend on a personal inbox or a paid mailbox provider.

All three sit behind Traefik on a self-hosted Proxmox/OPNsense DMZ, each with its own GitLab CI/CD pipeline, so a content change, a gateway change, and a mail change never end up blocking each other. While I was building it, I previewed work in progress through an ngrok tunnel, just a convenient way to share a live preview during development. That's since been swapped out for a permanent staging URL behind Traefik, which is one less thing to keep alive.

Sitting over the top of all of it is a self-hosted Prometheus, Grafana, Loki, and Alertmanager stack, deployed through its own pipeline so a monitoring tweak can ship without ever touching the signup flow. It keeps 30 days of metrics and logs, and routes 10 alert rules straight to Telegram, since that's the channel the committee already checks day to day rather than a dashboard they'd have to learn.

The gateway, in more depth

This is the part I'm proudest of, because none of it feels like the kind of corner-cutting you'd expect from a project with no budget. Member signup, the newsletter, and the contact form each run through their own Zod-validated flow backed by a SQLite state machine, so a half-finished signup or an unconfirmed newsletter subscription is always a known state rather than a guess. The newsletter itself uses proper double opt-in, so nobody ends up on a mailing list because of a typo or a bot.

Every public endpoint sits behind Cloudflare Turnstile and a 24-hour cooldown, with GDPR consent captured and timestamped the moment someone submits a form, rate limiting at 5 requests per IP per hour, and a CORS allowlist plus helmet keeping a tight lid on what the service will even talk to. There's also a quieter layer of defences working underneath all that, the kind that are deliberately invisible so an automated script can't tell they're there. The whole thing runs in non-root Docker containers with its own deploy pipeline, so shipping a gateway change never touches the site build.

Outcome

  • Replaced a WordPress site, manual Gmail-sent newsletters, and WhatsApp-invite admin with one coherent platform: a CMS-driven static site, an automated signup gateway with proper double opt-in, and self-hosted domain email.
  • Stood up a self-hosted observability stack so signup volume, abuse attempts, and third-party service health are visible without a paid monitoring tier.
  • Gave committee members a git-based editorial workflow through a vendored Sveltia CMS, no per-seat licensing, no third-party platform lock-in.
  • Replaced an early ngrok preview tunnel used during development with a permanent staging URL behind Traefik.

Operating targets

  • £0 marginal infrastructure cost. Cloudflare Turnstile, Resend, Telegram, and Umami free tiers, self-hosted on existing Proxmox/OPNsense hardware.
  • Rate limiting (5 requests/IP/hour), Turnstile, and a few quieter, unannounced defences on every public submission endpoint.
  • 30-day metric and log retention (Prometheus and Loki), with 10 alert rules routed to Telegram.
  • Independent deploy pipelines for the gateway, the site, the mail stack, and the observability stack.

Focus areas

  • Gateway design: an Express/TypeScript service (cyclekirklees-gateway) handling member signup, newsletter double opt-in, and contact form submissions, each with its own Zod-validated flow and SQLite-backed state machine
  • Site build: an Eleventy, Tailwind, and Webpack static site (cyclekirklees) for content, blog posts, and CMS-driven editorial
  • Domain email: self-hosted mail, inbound routing, and webmail for cyclekirklees.org.uk addresses
  • Security and abuse prevention: Turnstile, a CORS allowlist, helmet, GDPR consent capture, hidden bot defences, and non-root Docker containers
  • Observability and operations: the Prometheus/Grafana/Loki/Alertmanager stack, GitLab CI/CD with a DIND runner, systemd auto-start, and separate deploy paths for the gateway, site, mail, and monitoring stack

What I owned

I own this end-to-end: the public Eleventy site, the standalone gateway service, and the domain email stack, including the architecture, the Docker and Traefik deployment on a self-hosted Proxmox/OPNsense DMZ, the CI/CD pipelines, and the observability stack, all run to a £0 budget for an all-volunteer cycling campaign group.

Comments load automatically via GitHub (Utterances), which may set cookies. See the privacy policy.