How we run yrso
We keep yrso as simple as possible and make boring choices whenever we can. Our team needs to understand the system, maintain it and recover it when something breaks. Every extra service adds another dependency to that work.
The application runs on NixOS, uses Go on the server and stores its structured data in SQLite. Server configuration is reproducible, and database backups are files we can restore onto a replacement machine. Those choices make continuity more manageable for a small team.
Hosting and provider choices
We host our servers at OVH and Hetzner. Assets are currently hosted with Wasabi, and we are migrating that storage to Bunny as part of our move to EU-only vendors.
Repeatable server configuration helps us move workloads when our requirements change. Moving providers also involves transferring data and verifying that the application can use it in the new location.
Servers described in configuration
We use NixOS to describe the server setup in configuration files. That gives us a repeatable way to establish the operating environment on another machine. We can review a configuration change and retain the version that belongs to a release.
Our application build also uses Nix. Its build definition specifies the Go compiler and the tools used to generate templates and styles. A lock file records the Nix inputs, and the Go dependencies have a recorded hash. This makes the build inputs explicit and supports reproducible releases.
The server configuration selects a tagged application release. Deployment builds the selected NixOS configuration and switches the server to it. Disko describes the disk layout, and our provisioning tools use nixos-anywhere to install NixOS remotely. A replacement machine needs its hardware-specific settings adjusted, while the application and service definitions can be reused.
Reproducibility still depends on retaining the source and required dependencies. Secrets and data are restored separately because they change independently of the application build.
HTTPS and the application service
Caddy handles HTTPS and forwards requests to the Go application, which listens on the server’s local interface. Caddy also manages certificates for custom trust-portal domains, asking the application to approve a domain before issuing its certificate.
Systemd starts yrso when the server boots and restarts it if the process exits. The service runs with a dedicated dynamic user, a private temporary directory and restrictions on filesystem access. It can write its application state, but cannot modify the operating-system files or access users’ home directories. The service also prevents processes from gaining additional privileges.
Credentials and disk protection
We use agenix to manage encrypted secret files. The server makes the required values available to the application at runtime, separately from the application package. Restoring a server therefore includes restoring authorised access to those secrets.
Administrative SSH access uses keys, with password authentication and direct root login disabled. A host firewall limits incoming connections, and fail2ban adds protection against repeated failed login attempts.
The root filesystem uses LUKS encryption over mirrored disks. Encryption protects the stored root filesystem when locked; mirroring helps the server tolerate a disk failure. Backups remain necessary for recovery from deletion, corruption or loss of the server.
A Go application with server-rendered pages
Go handles requests and the application’s business logic. We use Chi for HTTP routing and templ to generate HTML. The compiled application embeds its static assets, which keeps those assets tied to the application release.
The browser interface uses HTMX for partial page updates, with Hyperscript and JavaScript for interactions. Tailwind CSS and DaisyUI provide styling. Most page rendering stays in the Go application, keeping the route, permissions and rendered response close together.
The application also runs scheduled jobs, including recurring evidence requests and reminders. That keeps the deployment compact. It means recovery and deployment need to ensure that the intended application instance is running the jobs, rather than accidentally starting duplicate schedules.
SQLite for application data
Yrso uses a control database and separate tenant databases. GORM provides database access, and Goose migrations record changes to the database structure. The databases are SQLite files on disk.
That makes the recovery assets straightforward: database backups are SQLite files. Restoring them does not require rebuilding a separate database server or a database cluster.
A live database needs a consistent backup. Copying a file while writes are in progress is not a reliable substitute for a database-aware backup procedure. SQLite documents its online backup API and other supported methods.
Uploaded evidence lives separately
Uploaded files currently use Wasabi’s S3-compatible object storage. The database holds the application’s records and references, while the storage service holds the uploaded objects. A recovery has to account for both, including during the migration to Bunny.
This separation keeps uploads outside the application release. It also means a database backup alone is not a complete copy of all customer information. Recovery needs the referenced objects and the credentials required to access them.
Reports, messages and diagnostics
Typst generates PDF reports and is included as a runtime dependency in the Nix package. Email uses SMTP for sending. Maddy receives incoming mail, which the application retrieves through IMAP. These connections allow evidence requests and responses to fit into existing working habits.
The application sends logs, metrics and traces to AppSignal through OpenTelemetry. These signals help connect a failed request or slow operation to the work the application was doing. The collector configuration is part of the deployment, and its credentials come from the encrypted secret file.
Recovery on a replacement server
The architecture supports a recovery sequence with concrete inputs:
- Provision a replacement server and apply the recorded NixOS configuration.
- Deploy the required application release with compatible database migrations.
- Restore consistent backups of the control and tenant databases.
- Restore access to the uploaded objects and supply the required secrets and service configuration.
- Verify data integrity and representative application operations, including login and evidence retrieval.
- Switch traffic when the checks pass, and confirm that scheduled work runs on the intended instance.
This greatly reduces the server setup work involved in recovery. It also makes the procedure easier to rehearse and hand over: the configuration and database files are explicit recovery inputs.
An operating-system rollback does not undo a database migration. Recovery has to match the application version with the restored data. The age of the available backups and the result of restore tests determine what can actually be recovered; the technology choices alone do not establish a recovery-time guarantee.
Boring choices we can maintain
We favour a small deployment with tools our team can inspect and operate. We add components when there is a demonstrated need, and account for the extra maintenance and recovery work they introduce.
The public marketing site is a separate Hugo site styled with Tailwind CSS. Keeping it separate lets us maintain the website independently of the application and its customer data.
Read about our ISO 27001-certified management system or the practical steps behind testing backups.