v0.9 self-hosted download · update · catalog Ed25519-signed, offline-anchored

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.

Every release is Ed25519-signed. Clients pin one root pubkey, fetch a signed trust list, refuse rollbacks and forgeries. A breached server can lie — it cannot impersonate. Phase C deployed on jenni.noschmarrn.dev, zero incidents.
01 · what she actually is

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 svn push 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.

02 · what's inside

Eight primitives.
Zero feature creep.
Single admin.

The whole binary fits in a Docker image you can audit in an afternoon. No SaaS dependency, no telemetry, no email flow, no „Pro plan" upsell. Your server, your password plus TOTP, ten single-use recovery codes, your CLI on the host as the last resort.
  1. 1
    Versioned releases with atomic switching

    Upload, activate, done. /<slug> always points to the latest version. Older versions stay reachable by exact version string until you prune them.

  2. 2
    JSON API for your own apps

    /api/projects/<slug>/latest returns version, size, SHA256, download URL, ClamAV scan status, scanner version, signature date. Schneespur asks Jenni once a day.

  3. 3
    Hardened uploads

    Magic-byte sniff before extension trust, zip-slip protection, zip-bomb cap, ClamAV scan on every upload. Vibe-coded doesn't have to mean reckless.

  4. 4
    WordPress plugin pipeline

    Optional pre-flight (PHPCompatibility, plugin-check) before publish. Optional one-click SVN push to wp.org for the plugins that live there. Master-password-encrypted credentials, atomic trunk + tags/<v> commit as a single revision, diff preview before the push. The non-WordPress projects ignore this entire branch.

  5. 5
    Module catalogs

    For apps that want their own plug-in ecosystem. Group releases under a collection slug; /api/modules/<collection> returns the full catalog — version, hash, icon, plus name, description, category and tags each in DE/EN, and a validated module.json manifest per module — with ETag caching. Your app fetches once a day, your users see what's available — no marketplace, no Stripe, no middleware.

  6. 6
    Embeddable widgets & trust badges

    Drop a small snippet on any external site. The widget asks Jenni for the latest version of your-app and renders a download button with the verified SHA256 and ClamAV status next to it. Themable (light/dark), language-aware (DE/EN). A separate badge.svg endpoint gives you a "scanned with ClamAV" badge for READMEs. Upload a new version on jenni.download — every embed updates automatically.

  7. 7
    Ed25519 trust layer for auto-updates

    Every public release gets a signed manifest with version, sha256, a monotonic counter, and a signed timestamp. A second key — the root — lives on the operator's laptop, never on the server, and signs the list of currently valid signing keys. Clients pin one 32-byte pubkey, fetch /api/signing/trust + /api/projects/<slug>/manifest, verify both, refuse rollbacks and forgeries. A breached server can lie; it cannot impersonate. Read the full trust model →

  8. 8
    Authenticated submission API

    Let third parties contribute modules to your catalog without hand-holding. Each bearer token is bound to exactly one collection — least privilege, nothing more. Authors POST …/submit, upload releases, manage screenshots; every upload runs the same ClamAV scan and Ed25519 signing as your own. Submissions land as drafts and you approve them (or flip a collection to auto-publish). Admin login is TOTP-2FA-protected and destructive actions need a step-up code. The full author API → JenniHUB, the self-service frontend on top of it, is coming.

03 · how it actually works

Two curls. The whole protocol.

~/projects/breznflow $
$ 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:

~/projects/breznflow $
$ 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.

04 · why I built this

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.

05 · still cooking

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