Cloud-hosted binary catalog + per-device config sync for NKS WebDev Console. The C# daemon pulls release metadata (Apache, PHP, MySQL, Redis, Caddy, cloudflared, …) from this service and backs up local site/service configuration so a fresh install hydrates from the last known good snapshot.
- ✅ Public JSON catalog —
/api/v1/catalogserved in the exact shapeCatalogClient.csexpects; drop in a URL and the daemon refreshes on startup - ✅ Full HTML admin panel — dashboard, user management (role + suspend + reset password), audit log + CSV export, invites (mint + redemption history), device browser, snapshot history with RFC 6902 diff, backup import / ZIP export, retention policy, global settings, revoked-token viewer
- ✅ RBAC with six roles — owner → admin → operator → support → user → readonly; account-level login lockout after 5 failed attempts
- ✅ Envelope-encrypted snapshots — AES-256-GCM with optional Variant B (passphrase-derived KEK for zero-knowledge mode)
- ✅ URL auto-generators — one-click scraping of upstream release listings (GitHub Releases API, php.net, apachelounge, nginx.org) with HEAD-probe verification before insertion
- ✅ Config sync — per-device JSON upload/download keyed by device ID for seamless re-install
- ✅ Observability —
/healthz(liveness),/readyz(DB + S3 deps),/metrics(Prometheus; optionally bearer-authed); structured JSON logs viapython-json-loggerwithX-Request-IDround-trip - ✅ OpenAPI 3.1 — auto-generated spec at
/openapi.json; drift checker (scripts/check-catalog-drift.mjs) validatesCatalogClient.csagainst the published contract - ✅ SQLite by default — swappable to Postgres via
DATABASE_URL - ✅ Docker-ready — published to GHCR on every tag (
ghcr.io/nks-hub/wdc-catalog-api:latest)
- Python 3.11 or higher (tested on 3.11 and 3.12)
- SQLite 3.35+ (included with Python) — or PostgreSQL 14+ for production
- Docker 24+ (optional, for containerised deployment)
run.cmdCreates a venv in .venv/, installs dependencies, and starts uvicorn on http://127.0.0.1:8765 with hot-reload. First run bootstraps an admin / admin account (dev mode — env var NKS_WDC_CATALOG_DEV=1 is set by the script).
POSIX / macOS:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
NKS_WDC_CATALOG_DEV=1 uvicorn app.main:app --host 127.0.0.1 --port 8765docker compose up -dService listens on http://localhost:8765. Catalog data mounts from ./app/data/apps so you can edit JSONs and POST /api/v1/catalog/reload without rebuilding.
Pre-built images are published to GHCR on every tag: ghcr.io/nks-hub/wdc-catalog-api:latest.
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
sqlite:///state/catalog.db |
SQLAlchemy connection string |
NKS_WDC_CATALOG_STATE_DIR |
./state |
Where SQLite + uploads live |
NKS_WDC_CATALOG_ADMIN_USER |
admin |
Bootstrap admin username |
NKS_WDC_CATALOG_ADMIN_PASS |
required in prod | Bootstrap admin password (dev fallback: admin) |
NKS_WDC_CATALOG_DEV |
— | 1 enables admin/admin fallback + verbose logs + ephemeral keys |
NKS_WDC_JWT_SECRET |
required in prod | HS256 signing key for desktop-client JWTs |
NKS_WDC_SESSION_SECRET |
required in prod | itsdangerous signer key for admin UI session cookies |
NKS_WDC_CATALOG_SECRET |
— | Legacy combined secret, fallback for both dedicated vars (deprecated) |
NKS_WDC_CATALOG_ALLOW_CORS |
— | 1 emits permissive CORS headers |
In production the secret env vars must be set to independent random 32+ byte values (openssl rand -base64 48). The service refuses to start otherwise. Setting NKS_WDC_CATALOG_DEV=1 overrides the check and generates ephemeral random keys — local development only.
In NKS WebDev Console → Settings → Advanced:
- Catalog URL:
http://127.0.0.1:8765(local) orhttps://wdc.nks-hub.cz(public NKS instance)
Or via env var when launching the daemon:
NKS_WDC_CATALOG_URL=http://127.0.0.1:8765
The daemon's CatalogClient fetches /api/v1/catalog on startup and caches the full release list in memory for the session. Use POST /api/binaries/catalog/refresh (authenticated) to pull a newer version without restarting the daemon.
GET /healthz
GET /readyz
GET /metrics
GET /api/v1/catalog
GET /api/v1/catalog/{app_name}
GET /api/v1/plugins/catalog
POST /api/v1/sync/config body: { device_id, payload }
GET /api/v1/sync/config/{device_id}
GET /api/v1/sync/config/{device_id}/exists
DELETE /api/v1/sync/config/{device_id}
GET /login POST /login POST /logout
# Overview
GET /admin dashboard (live counters)
# Catalog
GET /admin/catalog apps list
GET /admin/new new-app form
GET /admin/apps/{app_id} app + releases
POST /admin/apps/{app_id}/edit POST /admin/apps/{app_id}/delete
POST /admin/apps/{app_id}/releases add release (manual)
POST /admin/apps/{app_id}/auto-generate scrape upstream + insert
POST /admin/releases/{id}/delete
POST /admin/releases/{id}/downloads add download URL
POST /admin/downloads/{id}/delete
# Users + auth
GET /admin/users[?q=…&role=…] filterable list
GET /admin/users/{id} detail
POST /admin/users/{id}/role change role (+ token version bump)
POST /admin/users/{id}/suspend POST /admin/users/{id}/resume
POST /admin/users/{id}/reset-password renders new password once
POST /admin/users/{id}/revoke-tokens POST /admin/users/{id}/delete
# Invites
GET /admin/invites mint form
POST /admin/invites
GET /admin/invites/history consumed nonces
# Devices + snapshots
GET /admin/devices list scoped to caller's account
GET /admin/devices/{id} metadata + current payload
POST /admin/devices/{id}/delete
GET /admin/devices/{id}/snapshots browser w/ kind + label filter
GET /admin/devices/{id}/snapshots/{sid} detail + diff vs HEAD
POST /admin/devices/{id}/snapshots/{sid}/restore
GET /admin/devices/{id}/snapshots/export.zip
POST /admin/devices/{id}/import
# Operations
GET /admin/audit filterable log
GET /admin/audit.csv CSV export (same filters)
GET /admin/revoked-tokens JWT denylist
GET /admin/retention policy editor
POST /admin/retention/policy POST /admin/retention/run-now
GET /admin/settings GlobalPolicy editor
POST /admin/settings
GET /admin/account self-service page
POST /admin/account/password
| App | Source |
|---|---|
| cloudflared | github.com/cloudflare/cloudflared releases |
| mailpit | github.com/axllent/mailpit releases |
| caddy | github.com/caddyserver/caddy releases |
| redis | github.com/redis-windows/redis-windows releases |
| php | windows.php.net/downloads/releases/ HTML listing |
| apache | www.apachelounge.com/download/ HTML listing |
| nginx | nginx.org/en/download.html HTML listing |
| mariadb | archive.mariadb.org/ open-directory listing + nks-hub/webdev-console-binaries macOS probe |
| mysql | dev.mysql.com/downloads/mysql/ + nks-hub/webdev-console-binaries macOS probe |
| postgresql | nks-hub/webdev-console-binaries releases |
pytest # 550+ tests, coverage 85%+
pytest --cov=app # with coverage report
ruff format app/ tests/ # format
ruff check app/ tests/ # lintCI runs pytest + ruff + postgres compatibility on every push.
- Set
NKS_WDC_CATALOG_ADMIN_PASS,NKS_WDC_JWT_SECRET,NKS_WDC_SESSION_SECRETto independent 32+ byte random values - Run behind a TLS-terminating reverse proxy (Caddy, Traefik, nginx)
- Mount
/stateas a persistent volume so SQLite DB + config snapshots survive container restarts - Restrict
/api/v1/sync/config*behind an API key or Cloudflare Access header unless you want every device on the internet to write to your store - Scrape
/metricsinto Prometheus; ingest JSON logs with correlation IDs via your log aggregator
Contributions are welcome! For major changes please open an issue first.
- Fork the repository
- Create your feature branch (
git checkout -b feat/amazing-feature) - Keep
pytest+ruff check+ruff format --checkgreen - Commit your changes — one-line conventional commit messages, no AI attribution
- Open a Pull Request
- 📧 Email: dev@nks-hub.cz
- 🐛 Bug reports: GitHub Issues
- 🔗 Main project: nks-hub/webdev-console
MIT License — see LICENSE for details.
Made with ❤️ by NKS Hub