Building the Nant Gwrtheyrn App: Infrastructure for a Charity on a Budget

Building the Nant Gwrtheyrn App: Infrastructure for a Charity on a Budget

A detailed technical look at how the Nant Gwrtheyrn app works offline-first and how its backend runs on K3s Kubernetes, OpenTofu, and Packer for a fraction of the cost of a managed alternative. Covers the sync architecture, Restic backups, Prometheus and Grafana monitoring, and the iOS/Android accessibility work behind it.

Pete Stewart
Pete Stewart

The Nant Gwrtheyrn heritage centre sits at the bottom of a valley on the Llŷn Peninsula in North Wales. It’s one of the most beautiful places I’ve worked, and one of the more technically interesting, because the constraints were sharp and real.

The centre runs Welsh language courses and welcomes visitors to its nature trails. It’s a charity. Its infrastructure budget was not large. And the valley has unreliable mobile signal at the best of times, which means an app that falls back to a loading screen the moment connectivity drops would be, for all practical purposes, useless.

This is a writeup of how we approached the technical problem: the offline-first sync architecture, the backend infrastructure, how we kept costs low without compromising reliability, and the monitoring setup that means a small organisation can operate this without a dedicated ops team.


The core constraint: offline or nothing

Most apps treat offline support as a nice-to-have. For this project, it was the entire premise.

A visitor walking the headland trail shouldn’t need signal to see the next waypoint, read about the geology of the cliff face, or hear the Welsh name for the birds wheeling overhead. The app had to work completely (maps, navigation, content, Welsh language prompts) with no data connection whatsoever.

This shaped every other decision. The sync architecture, the content model, the database choice, the way we handle updates: all of it flows from that single constraint.

Offline-first, not offline-capable

There’s a meaningful difference between an app that works offline sometimes and one that’s built offline-first. The former starts with a network-connected model and adds fallbacks. The latter assumes no connection and treats connectivity as an enhancement.

We went offline-first. On first launch (or first launch after a significant update), the app downloads a complete content bundle: trail routes, waypoint data, points of interest, Welsh language content, and a set of map tiles covering the peninsula. After that, it runs entirely from local storage. Connectivity is used opportunistically (to check for content updates and pull down a delta if one exists) but it’s never required.

The app stores all content in a local SQLite database via [Room on Android / Core Data on iOS], and map tiles are cached using [map tile caching approach]. The content model is designed to be diffable: each piece of content has a version identifier, and the sync server returns only what’s changed since the client’s last sync.


The sync server

The sync server is a straightforward API: its job is to tell the app what’s new and serve content efficiently. It’s not complicated, deliberately.

Content versioning: Every content record has a version integer that increments on any change. The client sends its current version to the server; the server returns a payload containing only records with a higher version number. This keeps sync payloads small and makes resumable syncs trivial.

Content authoring: The Nant Gwrtheyrn team manage their own content (trail updates, new points of interest, corrections to Welsh text) through a simple CMS. The sync server sits in front of that data and handles formatting, bundling, and versioning for the client apps. The content team never touches the API directly.

Map tiles: Static map tiles are served from Cloudflare R2 rather than the API server, which keeps the API lightweight and makes tile delivery fast and cheap. R2 was an easy choice here: it’s S3-compatible (meaning standard tooling works without changes), and crucially, it has no egress fees. Serving map tiles means potentially a lot of data transfer: charging per gigabyte for that would add up quickly. R2 makes it a fixed, predictable storage cost instead. The app downloads a tile bundle covering the peninsula and caches it locally; updates to the tile set are handled separately from content updates.


Infrastructure: right-sized, not over-engineered

This is where it gets interesting, and where most of the cost and complexity decisions were made.

The instinct when you hear “Kubernetes” is to reach for EKS, GKE, or AKS. Managed Kubernetes from the major cloud providers is genuinely good, but it’s expensive, and for a charity running a content sync API with modest traffic, it’s significant overkill. A managed Kubernetes cluster on AWS or GCP would have cost more per month than the entire project’s infrastructure budget.

The alternative was K3s.

K3s: Kubernetes without the overhead

K3s is a lightweight, certified Kubernetes distribution from Rancher. It runs the full Kubernetes API (meaning everything you know about kubectl, deployments, services, ingress controllers, and persistent volumes works exactly as expected), but it’s packaged as a single binary and runs comfortably on a small VPS.

The sync server and its dependencies run in a K3s cluster on a [VPS provider, e.g. Hetzner, DigitalOcean] instance costing a fraction of a managed Kubernetes node. We get proper container orchestration (rolling deployments, health checks, automatic restarts, resource limits) without paying the managed control plane premium.

The practical benefits: deploying a new version of the sync server is a kubectl rollout with zero downtime. If the sync server crashes, Kubernetes restarts it automatically. Resource limits prevent a misbehaving process from starving everything else. These are things you’d want on any production service, and K3s makes them available at a price a charity can justify.

For ingress, we use [Traefik / nginx-ingress, K3s ships with Traefik by default], with TLS certificates managed automatically via cert-manager and Let’s Encrypt.

OpenTofu and Packer: infrastructure as code

The infrastructure is defined entirely in code. No clicking around in a cloud console, no manual server setup, no knowledge living only in someone’s head.

Packer builds the base server images. We define exactly what packages, configurations, and system-level settings we want, and Packer produces a reproducible machine image. If we ever need to rebuild from scratch (a new VPS, a disaster recovery scenario), we start from a known-good image rather than from a blank server.

OpenTofu (the open-source fork of Terraform) provisions the infrastructure: the VPS, DNS records, object storage buckets, firewall rules. The entire infrastructure (what exists, where it is, how it’s connected) is in a git repository. Changes go through the same review process as application code. Drift between the declared state and the real state is visible and can be corrected.

This combination matters especially for a project like this. There’s no internal ops team at the heritage centre. If something were to go wrong, or if I need to make significant infrastructure changes, having everything codified means I can understand exactly what’s running, make changes confidently, and reproduce the environment if needed. It also means handing the project to another developer in future is significantly less painful.

The cost: OpenTofu is free and open source. Packer is free. The investment is time upfront to write the configuration: time that pays back every time you make a change or need to understand why something is the way it is.

Cloudflare handles DNS for the project, managed through OpenTofu alongside everything else. Using Cloudflare for DNS means the sync server gets DDoS protection and Cloudflare’s global anycast network in front of it by default: useful insurance for a service that visitors are trying to reach from remote hillsides on patchy connections. DNS records are declared in the OpenTofu configuration, so changes to routing are versioned and reviewed like any other infrastructure change.


Backups with Restic

A heritage organisation’s content (trail routes, historical information, Welsh language text built up over time) has real value. Losing it to a disk failure or a mistaken kubectl delete would be genuinely damaging.

We use Restic for backups, with snapshots stored in Cloudflare R2. Restic is fast, efficient, and stores deduplicated, encrypted snapshots to any S3-compatible backend: R2 fits that perfectly, and using the same storage provider for both map tiles and backups simplifies the setup. Restic handles encryption before upload, so the data is encrypted client-side regardless of what R2 does with it at rest. Beyond deduplication and encryption, the restore process is straightforward, which matters more than it sounds: a backup you’ve never tested restoring from is a backup you don’t really have.

Backups run on a schedule via [a Kubernetes CronJob], covering the database and any locally-stored content. Snapshots are retained according to a policy that keeps daily backups for [X days], weekly for [X weeks], and monthly for [X months]: enough history to recover from anything short of an extended unnoticed corruption.

The key thing about Restic for a project like this: it’s a genuinely good backup tool that costs nothing to run (object storage costs are negligible at this scale) and can be operated without backup infrastructure expertise. It does what it says, and the restore process has been tested rather than assumed to work.


Monitoring: Prometheus and Grafana

Running infrastructure for a charity means there’s no on-call rotation. Nobody is going to get paged at 2am. If the sync server goes down at midnight on a Tuesday, we need to know about it, but we also need to set up monitoring in a way that doesn’t require constant babysitting.

Prometheus scrapes metrics from the sync server, K3s, and the underlying host: response times, error rates, database query performance, disk and memory usage, Kubernetes pod status. It stores a time-series record of the service’s behaviour.

Grafana visualises that data. We have a dashboard that shows the things that matter at a glance: is the sync server up, how many requests is it handling, what are response times like, is the disk filling up, when did the last backup run?

For alerting, we use [Alertmanager / Grafana alerting] to send notifications [via email / PagerDuty / Slack] when something crosses a threshold: the server becomes unreachable, error rates spike, disk usage exceeds 80%. The alerts are tuned to actually be useful: enough to catch real problems before they affect users, without generating noise that trains you to ignore them.

The monitoring stack runs in the same K3s cluster as the sync server. This is a deliberate choice: it’s simpler, and for this scale of deployment, a monitoring system that only works when everything else is also working is more useful than it sounds. If the entire cluster goes down, we’ll notice from the outside: uptime checks from [a simple external monitor] alert on that scenario.


Accessibility

Accessibility on the Nant Gwrtheyrn app wasn’t an afterthought: it was a design requirement from the start.

The Llŷn Peninsula is a destination for visitors of all ages and abilities. A trail app that doesn’t work for someone with a visual impairment, or that uses font sizes that older visitors can’t comfortably read, is failing part of its audience. A heritage organisation exists to be inclusive.

iOS: The app is fully compatible with VoiceOver. Every interactive element has a meaningful accessibility label. The trail navigation (waypoints, directions, points of interest) is navigable by gesture without relying on visual layout. Custom UI components are built using UIAccessibilityElement where needed to expose the right semantic information.

Android: TalkBack support follows the same principle: content descriptions on all meaningful UI elements, correct content grouping, and logical focus order. We use semantic roles and headings in the layout hierarchy so that TalkBack can describe the structure of the screen, not just its individual elements.

Text and contrast: Body text is set at a base size that scales with the system’s preferred text size: Dynamic Type on iOS, scalable text on Android. Contrast ratios meet WCAG AA throughout, including in the map overlays where coloured markers sit against a variable map background. This took more work than the rest of the accessibility effort combined, but it’s the thing most likely to matter to the most people.

Reduced motion: Animations in the app (transitions, map pan/zoom effects) respect the system’s reduced motion setting. This matters for users with vestibular disorders and is one of the lower-effort, higher-impact accessibility wins available.


What the infrastructure actually costs

I’m not going to give specific numbers because they change, and because every project is different. But the shape of it is worth describing.

The entire infrastructure (VPS running K3s, object storage for map tiles and backups, DNS) costs [in the range of €X–€Y per month]. That’s for a production service with proper container orchestration, automated backups, and full monitoring. It’s not the cheapest possible infrastructure, but it’s priced appropriately for what it delivers.

The alternative (managed Kubernetes, managed database, cloud-native monitoring) would cost several times more for the same functionality. For a for-profit product, that premium might be worth it in reduced operational overhead. For a heritage charity, the choice to invest the extra time upfront to configure K3s, OpenTofu, and Restic rather than pay for managed equivalents was straightforward.


What I’d do the same, and what I’d do differently

The same: offline-first as the foundational constraint. This decision made every subsequent technical choice simpler and clearer. It’s also the thing that makes the app actually useful in the field.

The same: OpenTofu + Packer. The time investment to write the infrastructure as code has paid back already, multiple times. It’s the reason I can make infrastructure changes with confidence and the reason the project isn’t dependent on institutional memory.

The same: K3s. It’s the right tool for this scale. Kubernetes semantics, fraction of the cost.

Differently: I’d instrument the app itself earlier. We added analytics and error tracking later in the project than we’d have liked, meaning we had less data about how the app was actually being used in the field during the early months. Instrumenting from the start (with appropriate privacy considerations) gives you much better information about where to focus your attention.

Differently: The content versioning scheme works, but a more structured approach to schema migrations (tracking content schema versions alongside content data versions) would have made some early content model changes less fiddly.


The Nant Gwrtheyrn project is a good example of what I try to do with every infrastructure decision: choose tools that are genuinely appropriate for the scale and the client, invest time upfront to do things properly, and end up with something that the organisation can rely on without needing to understand every moving part.

If you’re working on something with similar constraints (limited budget, resilience requirements, a small or non-technical team on the client side), I’d be happy to talk through the approach.

Get in touch

If you want to get in touch about a project, please send us an email.

Address
Tinkr Creative Dyfi Eco Parc Unit 1
Forestry Hub
Machynlleth
SY20 8AX
United Kingdom
Code Review and Audit

If you have a live project that isn't quite performing as expected, I offer remote code review and audit, and can then advise on performance optimisations.

Learn more

Mobile App Development

Cross-platform React Native apps for iOS and Android, built from one shared, accessible codebase.

Learn more

Website Performance Consulting

Digital consultancy can play a key part in the development cycle, especially in the early planning stages.

Learn more

Website Performance Optimisation

Many websites struggle with performance, so our tailored website performance optimisation is one of our most valued services.

Learn more

Website Development

Bilingual, accessible websites built mobile-first, with UX and performance at the core of every decision.

Learn more