Projects

Three systems I designed and built. They have genuinely different shapes, so each one is described on its own terms rather than squeezed into a shared template. Each opens with its architecture, because for anything here the diagram is the summary.

An uploader reaches a TLS edge and then the Nova coordinator, which holds keys and plaintext alongside Postgres metadata and an embedded IPFS node. Across a donor-blind boundary it replicates ciphertext to any number of donor nodes over a mutually authenticated mesh.
The boundary is the design. Everything sensitive stays left of the dashed line.

Nova

Federated, encrypted blob storage for sovereign, durable, private-by-default image hosting.

Private beta

A self-hostable, federated, content-addressed store for large binary objects. An operator runs one coordinator; volunteers donate storage without ever seeing what they are holding.

Operators hold the keys, donors hold opaque bytes

A site operator runs a single coordinator process. It speaks a plain HTTP API, holds the master key, and is the only place plaintext ever exists. Donor nodes replicate ciphertext over a mutually authenticated mesh and serve it back on read, and a donor can hold your data for years while learning nothing about it. The honest framing is that Nova is donor-blind rather than operator-blind: you pick an operator you trust, or you become one.

Every object is addressed by the hash of its ciphertext

Blobs are identified by the SHA-256 of the encrypted bytes, so the address is verifiable by anyone holding them and identical content never gets stored twice. Reads are ordinary cacheable HTTPS URLs, which means anything that accepts a URL integrates with Nova without knowing Nova exists.

Placement has to stay honest when machines disappear

This is the part I find genuinely hard. Replica placement is straightforward until you admit that two nodes can sit in the same building, on the same power feed, or behind the same ISP, and that any of them can vanish mid-write. Nova tracks a replica floor per object, keeps a reputation score per node, drops nodes that fall below the floor out of durability counts entirely, and re-replicates their objects onto nodes that have earned it. Repair is ordinary operation rather than an incident.

What is running today

The coordinator for my federation runs on a VPS I administer, with its landing page at nova.thebugreport.dev. Anyone can stand up a donor node against it, or run an entirely separate federation from the same source. The first product layer, nova-image, is drag-and-drop image hosting with on-the-fly transforms; video, audio, archive, and document layers are planned on the same storage core.

Sequence diagram of a write: the uploader sends an object through the TLS edge to the coordinator, which encrypts it, addresses it by the SHA-256 of the ciphertext, records placement in Postgres, pins it locally, replicates it to donor nodes over the mesh, commits the replica count, and returns a content-addressed HTTPS URL.
One write, end to end. Encryption happens before anything leaves the coordinator.
A reader request arrives at Caddy over HTTPS. Almost every request is answered from a file generated ahead of time. The few dynamic features cross a Unix socket into a small C FastCGI process which restricts itself with pledge and unveil and reads SQLite.
The thick path is the common one. The thin path is everything that can go wrong.

The Bug Report

Part indie web, part portfolio, part publishing platform.

Operating

The site you are reading. A custom static site generator, served by Caddy on OpenBSD, with the handful of interactive features split into separate C processes that can barely do anything at all.

Almost every request ends at a file

Articles and notes start as Markdown. A Python and Jinja generator renders them to HTML ahead of time, with SQLite carrying metadata. Nothing is assembled per request, so the fast path involves no application code at all, and the site stays fully readable with JavaScript disabled. Comments and the video embeds on the music page are the two deliberate exceptions.

C shrinks what the process is allowed to do at all

OpenBSD gives a process two ways to permanently give up power. pledge() narrows it to a declared set of syscalls; unveil() narrows it to a declared set of filesystem paths. A small C FastCGI binary that only reads a Unix socket and one directory can drop to roughly "stdio rpath" with a single unveiled path, so a bug in request parsing has almost nothing left to reach for. A Go or Python service cannot get there: their runtimes need threads, dynamic loading, a garbage collector, and a much wider view of the filesystem, so the same pledge is simply unavailable. C is what makes the small surface reachable.

The cost of that choice lands on testing

C gives no memory safety, so everything the language will not guarantee moves onto me. Parser probes, malformed-request cases, static analysis, and hardening builds are part of the design rather than a later pass. It is a deliberate trade, and I would make it again for a surface this small, but it is a trade and not a free win.

Component diagram showing Caddy routing to generated files and, over separate Unix sockets, to individual FastCGI services for search, listening state, contact handling, and backlinks, each reaching SQLite independently.
One process per socket, so a fault in one cannot reach the others.
The home network reaches the Cisco Catalyst switch over a wired link and the OpenBSD LattePanda over Wi-Fi. The switch connects an Orange Pi RV2 and an InnoMedia MTA, both on the bench.
Drawn to what exists. Solid is wired, dashed is wireless.

Homelab

Gadgets, Gizmos, and Things that go Wrrrr.

Operating

A compact Cisco switch, an OpenBSD machine that stages this site, a telephony bench, and the rented machines that keep the public side online.

The switch is what makes it a network

A Cisco Catalyst 3560CX-8PC-S anchors the wired side. The work I want from it is segmentation done properly: separate VLANs for voice, for the staging host, and for storage, with QoS where the voice path gives it an actual reason to exist. What interests me is not the command syntax but knowing where a boundary belongs and what specifically breaks when the policy is wrong.

Staging exists so production can be boring

A LattePanda 3 Delta runs OpenBSD and stages this site. Every change is built and tested there first, which matters because OpenBSD is particular about things Linux forgives, and I would rather find that out on a small board at home than on the machine readers are talking to.

The bench is allowed to be unfinished

A RISC-V board waiting on Asterisk, a telephony adapter waiting on the board, and a video scaler waiting on me to learn to solder. None of it is in service, which is the point of having a bench.

Deployment diagram separating home hardware from rented machines, showing the switch, the OpenBSD staging host, bench equipment, the web VPS running Caddy and C FastCGI, and the Nova coordinator with its Postgres database.
Home and rented estates fail, and get recovered, in different ways.

How I use AI tools

I am the primary human maintainer and technical decision-maker. I use Claude Code and Codex as development tools for implementation, review, and documentation; I remain responsible for requirements, architecture, verification, deployment, and published claims.