/api/healthLiveness and database probe. Returns 200 {"status": "ok", "version": "...", "db": "ok"}, or 503 {"status": "degraded", ...} if the DB probe fails.
Everything you'd want to read before running Jenni in production: every public endpoint and what it returns, the upload pipeline and why each step is there, the tech stack, the sizing, what she deliberately doesn't do, and where she fits next to GitHub Releases, WordPress.org, and a plain S3 bucket.
A self-hosted download server for versioned software releases — as ZIP, with a changelog, a virus-scan badge, anonymous statistics, and a JSON API. Single admin, small, fast, transparent.
She runs in production at jenni.noschmarrn.dev, ships every release of every project in this stack since the start of 2026, and is the source of the JSON your apps poll when they ask "is there a new version?".
| Audience | Why Jenni |
|---|---|
| Solo developers / indie makers | Ship under your own domain without GitHub Pages contortions, without a platform that owns your URLs. |
| WordPress plugin authors | Distribute beta or pro versions outside wp.org — and, with the SVN module, prepare and commit your wp.org releases in the same workflow. |
| Vendors with a module ecosystem | Your application loads a JSON catalog (/api/modules/<collection>) and shows users which extensions are available, with version, hash, icon, and category. |
| Small companies and clubs | Internal tools, training ZIPs, templates — admin login for the operator, public direct URLs for end users, audit log for everyone in between. |
| Self-hosters | Single-binary character, SQLite, no external dependencies beyond Python and Docker. Backup = copy two folders. |
Not for: multi-user SaaS, paid-download marketplaces with checkout, or massive multi-GB binaries. Multi-GB releases work but are not the sweet spot.
| Feature deliberately omitted | Reason |
|---|---|
| Multi-user / teams | Single-admin by design. If you need several co-maintainers, this is the wrong tool. |
| E-mail flow / "forgot password" | Password reset runs through the host shell (docker compose exec download python -m app.cli reset-password <user>). No SMTP setup, no phishing surface. |
| Frontend build step (React / Vue / …) | Server-rendered Jinja2. No webpack, no node_modules, no build pipeline. The container stays small (~617 MB including the PHP+SVN toolchain). |
| IP logging / user tracking | The downloads table stores only a timestamp and a release foreign key. No IP hash. No user-agent. GDPR-strict by construction. |
| DRM / paid-download gates | Out of scope. If you want paid downloads, put Jenni behind an auth proxy or a CDN layer. |
| Self-update for the app itself | Updates run through git pull + docker compose up --build. Single-admin hygiene, not a platform. |
Each of these tools fits a different shape of project. Jenni is the answer when you want one URL per software under your own domain, automated update endpoints across multiple platforms (not only WordPress), and the freedom to add module catalogs or a wp.org bridge without gluing four services together. The other tools win on platform discoverability or built-in edge caching — pick the one that matches what you actually need.
| Capability | Jenni | GitHub Releases | WordPress.org | S3 + CDN |
|---|---|---|---|---|
| One URL per software, under your own domain | Yes | — (lives at github.com) | — (lives at wp.org) | Yes |
| Discoverability via the host platform | — | Yes (GitHub network) | Yes (wp.org plugin directory) | — |
| Versioned permalinks | Atomic symlink switch | Git tags + assets | SVN tags | Self-managed |
| JSON update endpoint for your own apps | Yes (/api/projects/<slug>/latest) |
Yes (Releases API) | Yes (WP plugin update API) | — |
| Virus scan on upload | Yes (ClamAV per upload, status in API) | — | Manual review only | — |
| Anonymous download statistics (no IP, no UA) | Yes (GDPR-strict) | Aggregate only | Aggregate only | Provider-dependent |
| Embeddable widget for external sites | Yes (themed SVG) | — | — | — |
| Module catalog for app plug-in ecosystems | Yes (/api/modules/<collection>) |
Partial (org / repo listings) | WordPress only | — |
| WordPress wp.org SVN bridge | Yes (optional, atomic trunk + tags/ commit) |
— | Native | — |
| Edge caching globally | Reverse-proxy dependent | Built-in | Built-in | Built-in |
| Auto-update prompts in host ecosystem | — (your app polls) | — (your app polls) | Yes (wp-admin) | — |
| Cost model | Hosting only (~5 €/mo VPS) | Free (with LFS limits) | Free | Per-GB egress |
| Vendor lock-in | None (own VPS, own DB) | GitHub-hosted URLs | wp.org rules + queue | Provider |
| Type | Public URL | Notable behaviour |
|---|---|---|
standard |
/<slug> → ZIP |
The default. Any ZIP, any version sequence. Auto-detects version from filename or readme.txt. |
wp_plugin |
/<slug> → ZIP |
Adds a per-plugin settings panel for the wp.org SVN bridge. Optional pre-flight (PHPCompatibility, plugin-check, PHPCS), atomic trunk + tags/<version> commit as one revision, diff preview before push, optional initial pull from an existing wp.org plugin. |
module_collection |
/<slug> → 302 → JSON catalog |
A container, not a release. Lists its child modules through /api/modules/<slug> with ETag caching. ZIP uploads to a collection are rejected. |
module |
/<slug> → ZIP, /<slug>/icon → image |
Child of a collection. Activation requires category, image, and an active release. Cascade-protected: a collection cannot be deleted while modules still reference it. |
All four types share the same release table, the same scan pipeline, the same authentication, the same statistics, and the same audit log. The difference is in the public URL shape and the activation rules.
| Status | Visible to | Listed in /api/projects | Reachable via direct URL |
|---|---|---|---|
draft | Admin only | No | No |
public | Everyone | Yes | Yes |
hidden | Only by exact URL | No | Yes |
disabled | No one | No | 410 Gone |
Status changes are recorded in the audit log. Releases inside any project are soft-deleted — they leave the active release set but stay in the database, recoverable from the admin UI for as long as you want.
clean / infected / error / unscanned) is part of the public API and the info page.symlink swap. There is no read/write race on the active release.argon2-cffi).HttpOnly, Secure, SameSite=Lax, signed via itsdangerous.--proxy-headers.10001, never root.read_only: true root filesystem.cap_drop: [ALL].no-new-privileges security option.data/ and downloads/.
rescan-all is a CLI command intended to run nightly. It re-checks every existing release against the current ClamAV signature set, so a release that was clean on day one but matches a signature added later flips to infected in the public API the next morning.
| Concern | Choice |
|---|---|
| Language | Python 3.12 |
| Web framework | FastAPI + Uvicorn (ASGI) |
| Database | SQLite (WAL mode, foreign-key constraints active) |
| Templates | Jinja2 (server-rendered, no JS framework) |
| Password hashing | Argon2id via argon2-cffi |
| Sessions | Signed cookies via itsdangerous |
| Markdown | markdown-it-py + nh3 HTML sanitiser |
| Image validation (module icons) | Pillow magic-byte check + nh3 SVG sanitise |
| Virus scanning | ClamAV daemon, socket-mounted into the container |
| Deployment | Docker Compose behind a reverse proxy (Caddy / nginx / Traefik) |
Optional toolchain (for wp_plugin projects) | Subversion, PHP, Composer, WP-CLI, PHPCS / PHPCBF |
| Frontend build | None. Zero node_modules, zero JS framework. |
| Minimum host | 1 vCPU, 1 GB RAM. |
| Recommended host | 1 vCPU, 2 GB RAM (gives ClamAV room to breathe). |
| Image size | ~617 MB, including the PHP / SVN toolchain for wp_plugin projects. |
| Resident memory in steady state | well under 200 MB. |
| Reverse proxy (tested) | Caddy 2.x with ACME — example config in the repo. |
| Backup | Two directories: data/app.db via sqlite3 .backup, and downloads/ via rsync or restic. |
| Updates | git pull from the dev host, then docker compose up -d --build. Migrations run idempotently at lifespan-start. |
| Migrations style | Additive-only. Schema columns are never destructively renamed. |
| Live reference instance | jenni.noschmarrn.dev — production, currently on the Phase 6 build. |
All endpoints return UTF-8 JSON unless noted otherwise. Timestamps are ISO-8601. Hashes are hex-encoded SHA-256. Base URL throughout: https://jenni.download.
/api/healthLiveness and database probe. Returns 200 {"status": "ok", "version": "...", "db": "ok"}, or 503 {"status": "degraded", ...} if the DB probe fails.
/api/projectsLists every project with status public. One object per project.
[
{
"slug": "breznflow",
"name": "BreznFlow",
"description": "Short description",
"current_version": "1.2.3",
"size_bytes": 184320,
"sha256": "abc123…",
"uploaded_at": "2026-05-09T11:23:00",
"download_url": "https://jenni.download/breznflow"
}
]
/api/projects/<slug>Project detail. Returns also for hidden projects (only when accessed by exact slug). Adds full changelog (Markdown source + sanitised HTML) and scanner metadata.
{
"slug": "breznflow", "name": "BreznFlow",
"current_version": "1.2.3",
"size_bytes": 184320,
"sha256": "abc123…",
"uploaded_at": "2026-05-09T11:23:00",
"download_url": "https://jenni.download/breznflow",
"changelog": "## 1.2.3\n- Fix: …",
"changelog_html": "<h2>1.2.3</h2><ul><li>Fix: …</li></ul>",
"scan_status": "clean",
"scanner_name": "clamav",
"scanner_version": "1.4.3",
"scanned_at": "2026-05-09T11:23:05",
"scanner_signature_date": "2026-05-09T08:00:00"
}
/api/projects/<slug>/latestThe auto-updater endpoint. Same fields as /<slug> but only the release-specific subset — version, size, hash, download URL, changelog, scan status. The minimum your app needs to decide "do I update?".
{
"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"
}
/api/projects/<slug>/releasesAll non-deleted releases of a project, in upload order. Each entry carries version, size, hash, upload time, versioned download URL, changelog (raw + HTML), and scan fields.
/api/projects/<slug>/badge.svgTrust badge SVG for the active release ("Scanned with ClamAV …"). Cache-Control: public, max-age=300.
/api/projects/<slug>/releases/<version>/badge.svgTrust badge SVG for a specific historical version. Cache-Control: public, max-age=3600.
/api/modules/<collection-slug>The module catalog. ETag-based caching, 60 s max-age; returns 304 Not Modified on a quiet request.
{
"collection": {
"slug": "schneespur-modules",
"name": "Schneespur modules",
"description": "Extensions for the Schneespur app",
"generated_at": "2026-05-09T11:23:00Z"
},
"modules": [
{
"slug": "schneespur-export",
"name": "Export module",
"description": "GPX/CSV export",
"current_version": "0.4.1",
"category": "export",
"tags": ["gpx", "csv"],
"image_url": "https://jenni.download/schneespur-export/icon",
"website_url": "https://example.com/export-module",
"download_url": "https://jenni.download/schneespur-export",
"info_url": "https://jenni.download/schneespur-export/info",
"sha256": "abc123…",
"size_bytes": 51200,
"released_at": "2026-05-08T09:00:00Z"
}
]
}
These return application/zip, not JSON. Each download increments the anonymous counter.
/<slug>Active release ZIP. The most-quoted URL in the system.
/<slug>/v/<version>Specific historical version (unless soft-deleted). Same hashing and scanning guarantees as the active release.
/<collection-slug>For projects of type module_collection: 302 Found redirect to /api/modules/<collection-slug>. Collections are catalogs, not ZIPs.
/<slug>/widget.svgSelf-contained SVG widget. Themable via ?theme=light|dark, language-aware via Accept-Language + ?lang=de|en. Vary: Accept-Language, Cache-Control: public, max-age=300. CSP frame-ancestors: * (intended for embedding).
/<slug>/infoPublic HTML landing page for a project — metadata, changelog, size, hash, scan badge, manual download button. The non-API counterpart of the direct download URL.
/<slug>/v/<version>/infoSame shape, but for one specific historical version.
/<module-slug>/iconModule icon image. Strong sha256 ETag, Cache-Control: public, max-age=300. Pillow validates the magic bytes; nh3 sanitises any SVG.
trunk + tags/ commit.module_collection) and fourth (module), ETag-cached JSON catalog at /api/modules/<slug>, icon endpoint with sha256 ETag, cascade-protected deletion.assets/ path).Caddyfile, security CHANGELOG.Jenni is currently a personal project run in production for the operator's own releases. An open-source release under MIT is planned, gated on the self-install story being good enough for someone who isn't the operator to stand her up in an afternoon.
For now: if you want to run her, talk to me. If you want to evaluate her without running her, the live reference is jenni.noschmarrn.dev and every API endpoint above is reachable there.
Questions, integration ideas, audit notes — info@noschmarrn.dev.