Auto-updates
with an offline
trust anchor.
In Forrest Gump, Jenny is the reason Forrest runs. My Jenni — yes, with an i, deliberate — is the reason my updates verify.
jenni.noschmarrn.dev, zero incidents.
Six things in one binary.
- A self-hosted download server for versioned releases. Upload a ZIP, /your-app serves the latest.
- An update endpoint your apps poll. Tiny JSON: /api/projects/<slug>/latest returns version, SHA256, download URL, scan status.
- An Ed25519 trust layer on top of those updates. /api/signing/trust + /api/projects/<slug>/manifest — clients pin one root pubkey, verify every release, refuse rollbacks. How it works →
-
A WordPress plugin deploy pipeline.
Optional pre-flight (PHPCompatibility, plugin-check),
optional one-click
svnpush to wp.org, separate asset workflow for banner / icon / screenshots. - A JSON catalog for apps with their own plug-in ecosystems. /api/modules/<collection> returns every module with version, hash, icon URL — so your app can show its users what's available without you running a marketplace.
- A submission API for module authors. Collection-scoped bearer tokens let third parties POST /api/modules/<collection>/submit their own modules — same ClamAV scan, same Ed25519 signing — landing as drafts you approve. The author API → The self-service frontend on top, JenniHUB, is coming.
Replaces the Linux box where I used to type svn ci -m '…'
to ship plugins. Replaces the cron job that copied tarballs to a public bucket.
Replaces the half-finished script that was supposed to combine them.
Replaces the JSON-blob-in-a-gist that was pretending to be a module catalog.
Replaces the "I guess I'll just trust TLS" assumption that every
auto-updater secretly makes.
All in one place. Magic-byte sniffing, zip-slip protection,
zip-bomb caps, ClamAV scans on every upload, Ed25519 signatures
anchored to a key that never touches the server — and a few things I added because
they felt right at 2 a.m.
Two curls. The whole protocol.
$ curl https://jenni.download/api/projects/breznflow/latest
{
"version": "1.0.4",
"size_bytes": 107226,
"sha256": "d09e7e20442b789a4890beb2a8b2dd23a0adec62030d80a1828d533647eda13e",
"download_url": "https://jenni.download/breznflow",
"scan_status": "clean",
"scanned_at": "2026-05-09T02:03:20",
"scanner_name": "clamav",
"scanner_version": "1.4.3",
"scanner_signature_date": "2026-05-08T08:28:30"
}
Your app polls this endpoint. If version differs from what's
running, fetch download_url, check sha256, install.
That is the convenience shape. The widget on third-party sites uses the same call.
And if your app exposes a plug-in directory to its users,
/api/modules/<collection> returns the same shape, just for
many releases at once — each entry carrying its DE/EN strings and a validated
module.json manifest — ETag-cached, 304 on a quiet day, one round-trip on a busy one.
For auto-updaters that take signing seriously, the second curl carries a signature you verify against a key you pinned at install time:
$ curl https://jenni.download/api/projects/breznflow/manifest
{
"manifest": {
"project": "breznflow",
"version": "1.0.4",
"sha256": "d09e7e20442b789a4890beb2a8b2dd23a0adec62030d80a1828d533647eda13e",
"size_bytes": 107226,
"counter": 7,
"signed_at": "2026-05-14T09:21:33Z",
"key_id": "5af2613e6e92c56d",
"url": "/breznflow/v/1.0.4"
},
"signature": "BASE64 — Ed25519 over canonical(manifest)"
}
Cross-check key_id against the signed trust list at
/api/signing/trust. Verify the manifest signature with the
corresponding pubkey. Compare counter to the largest one
you have already accepted. If anything fails, refuse the update.
That is the whole protocol — the rest is implementation detail,
and the trust page walks you through every step.
Started as a habit, became a tool.
I was already self-hosting downloads — old habit, predates this stack. Zip the binary, drop it in a folder, link the URL. It worked. It was also messy.
When WordPress plugins joined the pile, the versioning side stopped being optional. Twenty plugins, twenty stable update endpoints, twenty hashes, twenty virus scans someone in legal can point to. The zip-in-a-folder pattern doesn't scale, and nobody needs another half-finished script to glue it together.
Then one of my apps wanted its own plug-in directory — a way to show users which modules existed without me running a marketplace, without Stripe, without a separate service. Same uploads, same scans, same hashing, just a different shape on the way out: a catalog endpoint instead of a single ZIP. That became the fourth pillar.
The fifth pillar showed up the day I read my own auto-updater code and noticed
it was trusting TLS for everything. If anyone ever pulled a shell on the
server, they could swap a binary, edit the hash field, and every client would
install it the next morning. The fix is older than the internet —
sign the metadata, pin the key — but the version I wanted had
two properties most rolled-your-own schemes don't: the signing key could
rotate without me shipping a client update, and the root key never had to live
on the same machine that signed the releases. Phase 7 added the per-release
manifests; Phase 8 added the offline trust anchor. The reference verifier
in the repo walks you through both, end-to-end, with raw cryptography
on the client side.
So I asked Claude to build me a download server. Then I read every line,
hardened the uploads, added ClamAV, added Ed25519, and ran her against my own stack at
jenni.noschmarrn.dev for months.
She's been the update endpoint for every release I shipped this year.
Vibe coding, when you actually look at the code afterwards, can ship
something solid. Jenni is exhibit A — and jenni.download is
where she'll live for everyone else.
She's running. She's been running. The offline trust anchor shipped in Phase 8 — and since then she learned to speak DE/EN (Phase 9), grew TOTP-2FA with a step-up code on every destructive action (Phase 10), and opened an authenticated submission API so third parties can contribute modules you approve (Phase C). The dated security changelog tracks every hardening choice with the day it landed on production, no marketing prose. What's still cooking is the install story for everyone else — a setup wizard, sane ENV defaults, a sample Caddyfile — and JenniHUB, the self-service frontend that turns that submission API into a marketplace. If anything above made you curious —
info@noschmarrn.dev