Doc · 02 · Administration

Administration.

What you do as an admin: bootstrap the repo, gate signups, moderate uploads, pick a retention policy, and watch the jobs & audit log. Everything below lives under /admin/*.

01Setup wizard

First-boot ritual at /admin/setup. Required before the F-Droid client can pull a signed index.

https://apks.example.com/admin/setup
Setup wizard at /admin/setup — pick Generate or Import to create the repo signing keystore

Generating a fresh key

  1. Pick the metadata

    The wizard reads KEY_DNAME from your env as the default Distinguished Name. Override if needed — this string ends up baked into the cert, so use real-looking values (CN, O, C).

  2. Generate

    The backend shells out to keytool against your container's JVM. Output is written to KEYSTORE_PATH inside the keystore volume — survives container restarts.

  3. Back up the keystore

    This is the most important secret in the system. If you lose it, every F-Droid client that pinned your repo certificate will refuse the next index — they can't recover. Treat it like a TLS key.

Importing an existing keystore

The wizard accepts .p12 (PKCS#12) and .jks. It needs the file plus the store password, the key alias, and the key password. After verifying the key responds to a test sign, it copies the file into the keystore volume.

WARN

Switching key mid-life will break all F-Droid clients that already pinned your repo. Plan for a complete client re-add when you do this.

02Repo settings

Under /admin/repo you control how the F-Droid index represents itself.

Name & description
Shown by the F-Droid client when subscribing to the repo. Free text in any language.
Repo icon
PNG, recommended 512×512. Surfaces in the F-Droid client header.
Address
The PUBLIC_REPO_URL baked into the signed index. Changing it forces every F-Droid client to re-add the repo.
Default retention
Maximum versions kept per app, repo-wide. 0 = unlimited. See §07.
Scan on upload
If ClamAV is wired up, every incoming APK is scanned before it lands. Refused uploads return 422.
Daily rescan
Re-scan every stored APK overnight. Findings show up in /admin/scans.

Reindex

The Trigger reindex button enqueues an arq job that rebuilds both the public and private indexes (index-v1.jar + index-v2.json + entry.jar). Watch the result in /admin/jobs.

Automatic reindexes (after every upload / publish / edit) coalesce into one run per minute — a burst of events folds into a single rebuild. The Trigger reindex button bypasses that dedup, so it always actually runs even if a recent automatic rebuild is still cached.

FIX

If apps you uploaded recently aren't showing in your F-Droid client, press Trigger reindex once. Older builds pinned every enqueue to the same job id, which arq dedupes against the 24h result key — silently swallowing every rebuild after the day's first. Fixed in 1.0.9; the manual button is the recovery for instances that already accumulated invisible uploads.

03Users & invites

/admin/users lists everyone with an account, with quick filters for admin, disabled, and OIDC-only. From there you can:

  • Promote a user to admin (or demote one — orphan-admin protection prevents you from removing the last admin).
  • Disable an account. They keep ownership of their apps; their sessions are revoked.
  • Force-rotate the password (sends them a reset; if local-only).
  • Delete the account. Their apps stay under the new owner you pick at deletion time.

Invites

With ALLOW_SIGNUP=false in .env, the public Sign up form is hidden — but you can still onboard users by minting an invite code.

https://apks.example.com/admin/users
User management at /admin/users — counts, filters, per-row controls

An invite can be open or bound to a specific email. Bound invites can only be redeemed by their email — useful when you don't fully trust the channel you're sharing the code through.

04Access & auth policy

/admin/access holds toggles that apply repo-wide:

Public signup
Mirrors ALLOW_SIGNUP. Off ⇒ only invites can create accounts.
Require TOTP for admins
Forces every admin to enrol a second factor. Existing admins are given a grace period before lockout.
Require passkey for admins / for uploaders
Two independent per-role toggles. When on, every account in that role must have at least one registered WebAuthn passkey to log in. Accounts caught between the flip and their first enrolment receive a short-lived enrollment_required token at the post-password step and are routed through a forced-enrolment screen.
OIDC auto-create
When on, first-time OIDC users auto-provision an account. Off ⇒ they need an invite or a pre-existing account.
Session lifetime
Override ACCESS_TOKEN_EXPIRE_MINUTES and REFRESH_TOKEN_EXPIRE_DAYS at runtime without restarting the backend.

05Categories

/admin/categories is a small CRUD page. Categories are surfaced both in the F-Droid client (as the section list) and in the web UI's filter rail.

  • Drag to reorder — that order is the one the F-Droid client renders.
  • Translations are stored alongside the slug; the UI uses the active locale.
  • Deleting a category un-assigns it from any apps that used it — the apps survive.

06Moderating apps

/admin/apps lists every app on the instance. Filters: pending review, private, has unscanned APK, orphaned.

APK lifecycle

When a maintainer uploads, the APK lands in pending. The admin queue at /admin/apps?status=pending shows it with:

  • Parsed manifest (package, version, min/target SDK, permissions list).
  • The signing certificate's SHA-256 — and a flag if it doesn't match the app's previously-accepted cert.
  • Latest ClamAV verdict, if scanning is on.
  • The maintainer's account and a quick link to their other apps.

Two actions:

  • Publish ⇒ the APK is included in the next reindex (which is auto-triggered).
  • Reject ⇒ the file is kept for audit but excluded from the index. Maintainer is notified.

App-level moderation

From an app detail page (admin view), you can:

  • Force the visibility (publicprivate) regardless of what the maintainer set.
  • Override the suggested version — useful if the latest upload has a known regression.
  • Transfer ownership to another user.
  • Hard-delete the app and all its APKs (cannot be undone — confirmed twice).

07Retention cap

fdroid-store doesn't keep an unbounded version history by default. Two layers of policy:

Repo default
Set in /admin/repo — applies to every app unless overridden. 0 = unlimited.
Per-app override
Set in /my-apps/{id} by an admin only. Can only tighten the repo default — never widen it. If the repo default is 5 and the override is 10, the effective cap is 5.

Eviction is FIFO by versionCode: when an upload would push the count above the cap, the oldest version is removed. The currently-suggested version is never evicted, even if it's the oldest.

RULE

A non-admin maintainer cannot bypass the repo default. The override field on the app page is admin-only — the UI hides it for everyone else.

08Scanning & quality

/admin/scanning (renamed from /admin/scans in v1.2) is the single page that hosts every APK-quality knob: malware scanning, vulnerability scanning, and Reproducible Builds verification. Each block surfaces its env-driven availability chip + the runtime toggle.

ClamAV — malware

When the clamav compose profile is active and CLAMAV_HOST is set:

  • Daemon health — green when the latest clamd ping returned within the threshold, red when it didn't. Click to ping now.
  • Scan on upload — refuses INFECTED uploads at the API door (422 + clamav.positive audit entry). The file is kept under quarantine/.
  • Daily re-scan — the worker re-checks every published APK at 03:00 UTC so signature DB updates catch previously-clean files retroactively.
  • Scan now — ignores the daily-cadence guard. Useful right after a signature DB update or to confirm wiring.
  • Findings history — every scan result with status (clean / infected / error / pending), signatures, the actor that triggered it.

Trivy — CVE / SBOM

When TRIVY_SERVER_URL points at a reachable trivy server (typically the bundled trivy compose profile — see install §07):

  • Auto-scan new uploads — every newly attached APK is queued for SBOM extraction (CycloneDX) + a vulnerability lookup. Results are private: owner / collaborator / admin only.
  • Manual re-scans are available from each APK's row in /my-apps/[id] (the side-sheet behind the Détails button shows the full CVE table).
  • Per-severity counts (CRITICAL / HIGH / MEDIUM / LOW / UNKNOWN) and the NVD-linked CVE table land in the SBOM record.

Reproducible Builds verification

Master switch for the per-APK RB verification feature (the badge on /apps/[package] and the editor on /my-apps/[id]). Defaults on; toggling it off:

  • 403s the POST /apks/{id}/reproducibility and verify-from-url endpoints with a clear "feature disabled" detail.
  • Hides the public badge on app pages and the per-APK editor in the version list.
  • Preserves historical data — the apks.reproducibility_* columns stay in the database, so re-enabling restores every prior verification untouched.

09Backup & restore

/admin/backup drives an asynchronous, encrypted, passphrase-protected backup of any subset of the four operational components:

  • DBpg_dump --format=custom of the whole Postgres database (users, apps, audit log, SBOMs, …).
  • Keystore — the .p12 signing key. The most critical component — lose it and every F-Droid client that already added your repo refuses to update past that point.
  • Assets — icons, screenshots, feature graphics, banners.
  • APKs — every published binary. The biggest by far; can be opted out for a config-only backup.

Workflow

  1. Open /admin/backup → tick the components you want → type a passphrase → start the job.
  2. The worker assembles the archive in BACKUP_TMP_DIR (the backup_tmp volume in the stock compose), encrypts it with AES-256-CBC + HMAC-SHA256 using a key derived from your passphrase, and posts the result.
  3. Progress is polled live; the same job appears in /admin/jobs with its trace.
  4. Once complete, the job lands in the history list with a Download button. The archive stays on the backup_tmp volume so subsequent downloads are instant — wipe the volume to free disk.

Restore

Same page: drop the encrypted .tar.enc archive, type the passphrase that produced it, tick which components to apply. The restore is selective — you can restore only the DB to recover from a bad migration without overwriting the storage volume, or only the keystore to recover a lost signing key while keeping the current data.

PASSPHRASE

The passphrase is never persisted. The archive header carries only the KDF salt and verification tag; without the passphrase the archive is cryptographically unrecoverable. Store the passphrase in a password manager separate from where you keep the archive.

RECOMMENDED

Take a backup right after the setup wizard mints the keystore, and again after every significant config change (categories, retention defaults, OIDC issuer). Keep at least one copy off-host.

10Jobs dashboard

/admin/jobs is the operator's pane of glass over arq.

https://apks.example.com/admin/jobs
Jobs dashboard — Redis link, queued/running/last-5-min counters, recent fetch_github_source runs

Listed jobs:

  • rebuild_index — the F-Droid index builder. Fires after every accepted upload + on manual trigger.
  • scan_apks_periodic — daily ClamAV sweep (default 03:00 UTC).
  • scan_apk_cve — per-APK Trivy scan, enqueued automatically after every upload when CVE scanning is enabled.
  • scan_github_sources_periodic — coordinator that fans out per-source fetches.
  • fetch_github_source — per-app forge polling + import + eviction + reindex.
  • create_backup / restore_backup — the async backup pipeline (see §09). Heavy-IO jobs that produce / consume the encrypted archive on the backup_tmp volume.

Each row has a re-run link that requeues the same job with the same args. Failures show their traceback in a side drawer.

11Audit log

Every privileged action lands here at /admin/audit. Examples:

  • auth.login, auth.login.mfa, auth.logout, auth.refresh
  • user.created, user.disabled, user.role_changed
  • app.created, app.visibility_changed, app.transferred, app.deleted
  • apk.uploaded, apk.published, apk.rejected, apk.evicted
  • api_key.created, api_key.revoked
  • deploy_token.created, deploy_token.revoked, deploy_token.used
  • github_source.upserted, github_source.scanned
  • clamav.positive, clamav.cleared
  • apk.reproducibility_updated — every write to an APK's RB status, including the matched reference hash and URL.
  • apk.cve_scan_* — SBOM extraction + CVE lookup outcomes.
  • webauthn.credential_added, webauthn.credential_removed, auth.login.passkey
  • backup.job_created, backup.job_completed, backup.restored — the encrypted-archive workflow (components included, byte size, who triggered it).
  • repo.config_updated — every flip of the scanning / RB / WebAuthn force toggles is captured with the full payload.

Filter by actor, target, action prefix, IP, time range. Each row carries a structured payload field — token prefixes, state transitions, file hashes — but never plaintext credentials.

EXPORT

The audit page offers a CSV export of the current filter. The same data is available via GET /api/v1/admin/audit for piping into your own SIEM.

12Source proxies

/admin/proxies manages the registry of external source proxies — small HTTP services that fetch APKs from places fdroid-store doesn't scrape natively (Patreon, F-Droid mirrors, in-house artefact repos…). The proxy owns the legal/ToS/maintenance burden for its slice; fdroid-store stays neutral and only speaks the v1 proxy protocol over plain HTTP.

Registering a proxy

  1. Deploy the proxy

    The reference F-Droid proxy ships in the compose stack under the proxy-fdroid profile (see install §08). Third-party proxies (Patreon, internal registries, …) are out-of-tree — see the protocol spec to write your own.

  2. Add it

    Click Ajouter on /admin/proxies, name it, paste the base URL and the shared secret your proxy expects. fdroid-store immediately hits GET /healthz and caches the proxy's GET /sources catalogue (which providers it exposes, what auth each needs).

  3. Health chip

    Each row shows OK / Unreachable / Auth refused / Bad response based on the last probe. Tester re-runs the health check + catalogue refresh on demand.

What admins see (and uploaders don't)

The proxy URL, shared secret status, health detail, and timestamps live on /admin/proxies only. The GET /api/v1/proxies endpoint that uploaders hit returns a slim schema — {id, name, cached_sources_json} — so internal hostnames, custom ports, and operational chatter never leak past this page. Uploaders pick a proxy by name; the actual HTTP traffic happens server-side.

Auth modes

What each provider tells uploaders to fill, declared in the catalogue:

none
Open source (e.g. F-Droid public repo). Uploader pastes a URL, that's it.
api_token / basic
Uploader pastes one or more secret fields (label + placeholder declared by the provider). Stored Fernet-encrypted on the source row; only sent to this proxy on a scan.
oauth2
Uploader clicks Connect, a popup opens against the proxy's begin endpoint, the proxy handles the IdP dance, then redirects back to /api/v1/auth/proxy-callback with a credential_id. fdroid-store stores the opaque id — the access/refresh token stays on the proxy. State is HMAC-signed with a key derived from SECRET_KEY.

Cron + audit

The worker runs scan_apk_proxy_sources_periodic at 05:00 UTC — one job per enabled source. retry_after from a 429 is honoured (capped at 24 h so a hostile proxy can't pause sources for years). Every transition writes a proxy_source.* audit entry (created, updated, deleted, imported, sha256_mismatch, import_failed). SHA-256 mismatches between the proxy's apk_sha256_hint and the downloaded bytes are logged as their own row — strongest signal of a compromised upstream link.

SAFETY

The proxy's apk_url is run through the same SSRF guard as forge downloads: RFC 1918, loopback, link-local, and cloud-metadata IPs are refused. The advertised apk_size_bytes is enforced during streaming so a proxy lying about the file size can't fill disk. The manifest's version_code and package_name are belt-and-suspenders-checked against the proxy's advertised values — a mismatch rejects the import.