Distribution & Install Guide

August 3, 2026 · View on GitHub

The canonical guide to every supported way of installing and operating LibreDB Studio, and to how the release pipeline publishes each channel. For a one-line-per-channel overview, see the Install matrix in the README.

ChannelBest forSection
DockerServers, PaaS, CI, quickest startDocker
HelmKubernetesHelm (Kubernetes)
npxTrying it on a laptop with Node installednpx
HomebrewmacOS / Linux workstationsHomebrew
.deb / .rpmDebian/Ubuntu and RHEL/Fedora servers (systemd)Linux packages (.deb / .rpm)
SnapUbuntu and other snapd systemsSnap
Windows (winget / Chocolatey / portable zip)Windows workstationsWindows
Desktop app (AppImage, .deb, FlatPark)Linux desktops - an application window, no browser tabDesktop app

All non-Docker channels ship or download the same standalone server payload (Next.js standalone output, started with node server.js) built by scripts/build-standalone-payload.sh and attached to GitHub releases by .github/workflows/release-artifacts.yml. Standalone artifacts (tarballs, .deb/.rpm, Homebrew formula, snap) are published on release, starting with the first release that includes the release-artifacts workflow — older releases have Docker images only.

Runtime note: every channel here runs the production server under Node (node server.js), including the Docker image — its runner stage is node:24.16.0-trixie-slim and CMD execs node server.js; Bun is only used to install dependencies during the Docker build and for local development (bun dev). The SQLite DB provider adapts to whichever runtime it finds (bun:sqlite under Bun, node:sqlite under Node) — see docs/providers/sqlite.md, Runtime & driver selection.

Zero-config first run

Every channel works with no configuration. When JWT_SECRET / ADMIN_PASSWORD are not set (and the auth provider is not OIDC), the server generates them on first start, persists them in <data dir>/auth-bootstrap.json (file mode 0600), and prints the admin password once to the server log:

============================================================
 LibreDB Studio first run: generated admin credentials
 Email:    admin@libredb.org
 Password: <generated>
 Stored in <data dir>/auth-bootstrap.json (delete the file to regenerate)
============================================================
  • The data dir is the directory of STORAGE_SQLITE_PATH (default ./data inside the payload / container). Each channel below documents where that is and how to read the log.
  • Explicitly set environment variables always take precedence; only missing values are generated.
  • If the data dir is not persisted (ephemeral container, no volume), new credentials are generated on every recreate.

Strict mode: set AUTH_BOOTSTRAP=off to disable generation and require explicit JWT_SECRET and ADMIN_PASSWORD (recommended for production; missing values then surface as a clear error on the login page instead of silently generated credentials in collected logs). Every channel, the Helm chart included, defaults to zero-config; strict mode is always opt-in. Unrecognized AUTH_BOOTSTRAP values log a warning and keep bootstrap on.

Network exposure (bind address)

Every native channel is local-first: the server binds to 127.0.0.1 by default, and exposing it on the network is an explicit opt-in. (The Docker image and the Helm chart are the exception - containers must bind 0.0.0.0 and are isolated by container networking instead.)

ChannelDefault bindHow to expose
npx127.0.0.1npx @libredb/studio --host 0.0.0.0 (or set HOSTNAME)
.deb / .rpm (systemd)127.0.0.1HOSTNAME=0.0.0.0 in /etc/libredb-studio/env, then restart
.deb / .rpm (direct run)127.0.0.1LIBREDB_BIND=0.0.0.0 libredb-studio
Homebrew service127.0.0.1run the binary manually with LIBREDB_BIND=0.0.0.0, or front it with a reverse proxy
Snap127.0.0.1sudo systemctl edit snap.libredb-studio.libredb-studio.service with [Service] Environment=HOSTNAME=0.0.0.0
Docker / Helm0.0.0.0 (container-internal)publish/route ports as usual (-p, Service/Ingress)

For anything reachable from a network, prefer a reverse proxy with TLS in front and strict mode (AUTH_BOOTSTRAP=off) with explicit credentials.

A direct run of the .deb/.rpm wrapper or the Homebrew binary ignores any inherited HOSTNAME (empty, or - under Docker - the container ID Next.js would otherwise bind to) and defaults to loopback; LIBREDB_BIND is the explicit opt-in for that case. Under systemd, HOSTNAME in /etc/libredb-studio/env is still the override, since the unit resolves it before the wrapper runs (detected via the systemd-set INVOCATION_ID, so the wrapper leaves it untouched there).

Release artifact naming

Release tags carry no v prefix (tag 0.9.41 == package.json version). Each release ships:

ArtifactNameTargets
Standalone server tarballlibredb-studio-standalone-<version>-<os>-<arch>.tar.gzlinux-x64, linux-arm64, darwin-x64, darwin-arm64
Standalone server zip (Windows)libredb-studio-standalone-<version>-win32-x64.zipwin32-x64 (bundled Node runtime + libredb-studio.exe launcher)
ChecksumsSHA256SUMScovers all standalone tarballs and the win32 zip
Debian packagelibredb-studio_<version>_<arch>.deb (+ .sha256 sidecar)amd64, arm64
RPM packagelibredb-studio-<version>.<arch>.rpm (+ .sha256 sidecar)x86_64, aarch64
Snaplibredb-studio_<version>_<arch>.snapamd64, arm64 (also published to the Snap Store)
Desktop AppImagelibredb-studio-desktop-<version>-linux-<arch>.AppImage (+ .sha256 sidecar)x64, arm64 (also the artifact the in-repo Flatpak manifest repacks)
Desktop Debian packagelibredb-studio-desktop_<version>_<arch>.deb (+ .sha256 sidecar)amd64, arm64 (from 0.9.62; the artifact FlatPark pins as extra-data)

SHA256SUMS covers the standalone tarballs and the win32 zip; each .deb/.rpm/.AppImage ships its own per-file <artifact>.sha256 sidecar instead (those are built in separate jobs).

Two different .debs ship per release and they are not interchangeable. libredb-studio_<version>_<arch>.deb is the headless server: it installs a systemd unit and is built by nfpm from packaging/linux/nfpm.yaml. libredb-studio-desktop_<version>_<arch>.deb is the GUI app: a desktop entry, the Tauri shell, the pinned Node sidecar and the server payload, built by the Tauri bundler in the same job as the AppImage. Different file names and different dpkg package names (libredb-studio vs libredb-studio-desktop), so both can be installed on one machine.

Standalone tarball entries are rooted under a top-level libredb-studio-<version>/ directory (not a tarbomb) - extract with tar --strip-components=1 (tar xzf <artifact> --strip-components=1), which is what the npx launcher, the deb/rpm/snap packaging jobs, and scripts/build-standalone-payload.sh's own --smoke self-test all do; Homebrew strips the single top-level directory automatically for its main url/sha256 download, so the formula needs no extra flag.

The win32 zip is deliberately FLAT (entries at the archive root, no versioned wrapper directory - scripts/lib/pack-standalone-zip.sh): winget resolves the manifest's NestedInstallerFiles.RelativeFilePath against the zip root and wingetcreate update never rewrites that path, so it must stay libredb-studio.exe across releases; Chocolatey likewise unzips straight into its package tools directory. Windows Explorer's "Extract All" already defaults to a folder named after the zip, so manual extraction stays tidy.

The payload contains only the runtime (server.js, package.json, .next, node_modules, public, an empty data/, plus LICENSE/README.md): Next.js output file tracing sweeps the repo root into .next/standalone, so payload assembly prunes the non-runtime extras (docs, source, tooling configs, deploy manifests, local build leftovers) via a deny-list (scripts/lib/prune-standalone-payload.sh). The deny-list covers project-conventional paths only - on a local (non-CI) build, arbitrary personal files sitting at the repo root can still be traced in, so keep secrets out of the repo root (CI release checkouts are clean; published artifacts are unaffected).

Download URL pattern: https://github.com/libredb/libredb-studio/releases/download/<version>/<artifact>.

Docker

ghcr.io/libredb/libredb-studio is the canonical image (no pull rate limits). Docker Hub (libredb/libredb-studio) is a discoverability mirror only.

# Zero-config: the first run prints the generated admin password to the log
docker run -d --name libredb-studio -p 3000:3000 \
  -v libredb-data:/app/data \
  ghcr.io/libredb/libredb-studio:latest

docker logs libredb-studio   # shows the first-run credentials banner

The /app/data volume persists the generated credentials and the server-side SQLite storage; without it, a recreated container generates new credentials.

Production (strict mode, explicit secrets):

docker run -d --name libredb-studio -p 3000:3000 \
  -e AUTH_BOOTSTRAP=off \
  -e JWT_SECRET=change-me-to-a-random-32-char-string \
  -e ADMIN_EMAIL=admin@libredb.org \
  -e ADMIN_PASSWORD=your_secure_admin_password \
  ghcr.io/libredb/libredb-studio:latest

All environment variables are documented in .env.example; a ready-to-use compose file is docker-compose.example.yml.

Image tag model

Published by .github/workflows/docker-build-push.yml:

TagPublished fromMutability
<version> (e.g. 0.9.41)GitHub release (or manual dispatch) onlypinned, never overwritten by branch pushes
latestGitHub release onlymoves on each release
mainevery push to main (including PR merges)moving pre-release tag
devevery push to a feat/** / fix/** branchmoving development tag
sha-<commit>every buildimmutable

Use <version> or sha-<commit> for reproducible deployments; main / dev are for testing unreleased code.

Architectures: every tag published from main, a release or a manual dispatch is a linux/amd64 + linux/arm64 manifest. Branch previews (dev and the sha- tag of a feat/** / fix/** push) are linux/amd64 only — the build job has no native arm64 runner, so arm64 goes through QEMU and dominates the job's runtime; spending that on every commit of an open PR buys nothing, since the only consumer of dev is the amd64 Channel E2E gate. Pull main (or a released version) when you need arm64 from an unreleased line.

Helm (Kubernetes)

helm repo add libredb https://libredb.org/libredb-studio/
helm install libredb libredb/libredb-studio \
  --set secrets.jwtSecret=$(openssl rand -base64 32) \
  --set secrets.adminPassword=MyAdmin123

Or from the OCI registry:

helm install libredb oci://ghcr.io/libredb/charts/libredb-studio \
  --set secrets.jwtSecret=$(openssl rand -base64 32) \
  --set secrets.adminPassword=MyAdmin123

The chart defaults to zero-config bootstrap like every other channel (config.authBootstrap: "" omits the variable, so the app default applies): helm install works with no values at all. For production, inject real secrets as above, or enforce them with strict mode (--set config.authBootstrap=off).

The chart's own README.md is the canonical description of that behaviour — credential retrieval, what strict mode requires per auth provider, persistence and the single-replica constraint. Full values reference: charts/libredb-studio/README.md; chart architecture: docs/HELM_CHART.md.

OpenShift operator (OperatorHub)

operator/ packages the published Helm chart as a codeless helm-operator (operator-sdk helm plugin): a LibreDBStudio custom resource whose spec mirrors the chart values. Publishing works in two stages:

  • Controller image.github/workflows/operator-release.yml builds and pushes ghcr.io/libredb/libredb-studio-operator:<version> (amd64+arm64) on every app release. Chart releases (libredb-studio-x.y.z tags) are skipped quietly. The embedded chart's declared dependencies are vendored with helm dependency build right before the image build; charts/*.tgz is never committed.
  • Catalog bundleoperator/bundle/ (generated by make -C operator bundle, which stamps the versioned containerImage CSV annotation and validates with --select-optional suite=operatorframework — the same suite the community pipelines run; no validator flags a missing containerImage, so the stamp is enforced by the Makefile, not by validation) is the submission source for both community catalogs: k8s-operatorhub/community-operators (operatorhub.io, plain bundle-directory PR) and redhat-openshift-ecosystem/community-operators-prod (OpenShift console). The community-operators-prod submission uses the FBC contribution mode (fbc.enabled: true plus a per-OCP-version catalog_mapping in the submission's ci.yaml): bundle-directory PRs without FBC are still accepted, but FBC is the pipeline's recommended mode and automates promotion into each new OpenShift version's catalog (version_promotion_strategy, default review-needed). Channel operatorhub-community in distribution/channels.yaml tracks it (status: pending until the first listing merges; tracking issue #152).

Two copies are kept honest by CI: the embedded chart copy (operator/helm-charts/) is enforced by the required chart:check gate (chart:bump refreshes it), and bundle freshness (operator/config vs the generated operator/bundle) is checked in the Helm Chart Lint job whenever a PR touches operator/. The operator Makefile derives VERSION from package.json, so there is no fourth hand-maintained version string.

An FBC release is two upstream PRs

A community-operators-prod release in FBC mode always takes two merges, and only the first one is triggered by the bundle:

  1. Bundle PRoperators/libredb-studio-operator/<version>/ (a copy of operator/bundle) plus ci.yaml, Makefile and catalog-templates/. Merging it runs the release pipeline, which publishes quay.io/community-operator-pipeline-prod/libredb-studio-operator:<version>. The operator is released at this point but is in no catalog yet, so nothing is installable from the OpenShift console.

  2. Catalog PR — the rendered catalogs/v4.15..v4.22/libredb-studio-operator/catalog.yaml files, one per OCP version in the catalog_mapping. Render them from a fork's checkout:

    cd operators/libredb-studio-operator
    make catalogs           # opm alpha render-template, driven by ci.yaml
    make validate-catalogs  # opm validate; must pass for every mapped version
    

    The renderer applies --migrate-level bundle-object-to-csv-metadata only for v4.17+, which is why the pre-4.17 catalogs keep the base64 olm.bundle.object form and the newer ones do not.

rh-operator-bundle-bot opens that second PR by itself (updating both the template and the rendered catalogs) only when the bundle version directory carries a release-config.yaml; otherwise the release pipeline just prints the manual instructions in its summary comment. 0.9.59 shipped without one, so its catalog PR (community-operators-prod#10581) was rendered and opened by hand. From 0.9.60 on, include this file in the bundle PR as operators/libredb-studio-operator/<version>/release-config.yaml:

---
catalog_templates:
  - template_name: basic.yaml
    channels: [alpha]
    replaces: libredb-studio-operator.v<previous version>

replaces wires the new bundle into the update graph and is omitted only for a first submission. version_promotion_strategy in ci.yaml is a separate knob (promotion into each newly added OpenShift version's catalog) and does not substitute for release-config.yaml. The operatorhub.io submission (k8s-operatorhub/community-operators) has no such second step: its bundle-directory PR is the whole listing.

npx

Requires Node.js 20.9+ on Linux, macOS (x64 / arm64), or Windows (x64); Node 24 LTS is the recommended, fully supported runtime. The launcher checks the runtime up front and spells out what an older Node cannot do (scripts/engine-smoke.sh tests every tier in CI):

NodeSupport
24+ (recommended)Everything works
22.13 - 23.xWorks; server-side SQLite storage (STORAGE_PROVIDER=sqlite) needs Node 24 (the bundled better-sqlite3 binding targets the Node 24 ABI) and fails with a clear error. node:sqlite may print a one-time ExperimentalWarning.
20.9 - 22.12Works, minus all SQLite features: SQLite database connections need the built-in node:sqlite (unflagged from Node 22.13).
< 20.9Refused with a clear error (Next.js 16 floor) - bare npx may also fall back to an ancient, bin-less package version here; those versions are npm-deprecated with pointers.

The npm package stays a pure library for libredb-platform; the launcher downloads the matching standalone archive (tar.gz; the flat zip on Windows) from the GitHub release, verifies it against the SHA256SUMS release asset, caches it under ~/.libredb-studio/<version>/, and starts node server.js:

npx @libredb/studio                # first run downloads + verifies, then starts
npx @libredb/studio --port 8080    # or set PORT
npx @libredb/studio --help

--archive starts from a local tarball and skips checksum verification unless you pin a digest with --archive-sha256 <hex> - only use archives you built yourself or obtained from a trusted source. Downloads retry transient failures with backoff and abort when the connection stalls. The cache in ~/.libredb-studio/<version>/ lives in your home directory's trust domain; --verify-cache re-checks the cached tarball against the cached SHA256SUMS and re-extracts the payload. Re-extraction (--verify-cache and every --archive run) preserves the payload's data/ directory - generated auth-bootstrap.json credentials and any STORAGE_PROVIDER=sqlite state survive across it, so a re-verify or a rebuilt archive never invalidates a previously printed admin password.

  • Later runs start straight from the cache (per-version directory; delete it to force a re-download).

  • All environment variables are forwarded to the server (PORT, HOSTNAME, JWT_SECRET, ADMIN_PASSWORD, AUTH_BOOTSTRAP, STORAGE_PROVIDER, STORAGE_SQLITE_PATH, LLM_*, ...). Missing auth secrets are handled by the zero-config first run, which prints the admin password to the terminal.

  • --archive <path> (env: LIBREDB_STUDIO_ARCHIVE) starts from a local standalone tarball instead of downloading — useful for testing a tarball built with scripts/build-standalone-payload.sh (checksum verification is skipped and the archive is re-extracted on every run).

  • On Windows the launcher downloads the win32 zip and extracts it with the built-in System32\tar.exe (bsdtar); see Windows (issue #114).

  • Versions released before the standalone tarballs existed have no artifacts; the launcher detects this (HTTP 404) and suggests npx @libredb/studio@latest.

  • The npm package is published with npm provenance (npm publish --provenance in npm-publish.yml, from 0.9.63). npmjs stores a Sigstore attestation naming the repo, workflow and commit that built the tarball, so npm audit signatures verifies the package against a trust root outside the release:

    npm i @libredb/studio && npm audit signatures   # "verified registry signatures / attestations"
    

    This covers the package you install from npm. The standalone archive the launcher downloads from the GitHub release carries its own SLSA attestation, and from 0.9.63 the launcher checks it - see below.

Launcher provenance check

After the checksum passes, the launcher runs gh attestation verify on the archive, pinned to the workflow that signs releases (--signer-workflow …/release-artifacts.yml, so an attestation from any other workflow or a branch build fails the policy). It runs where checksum verification runs: on a fresh download and on --verify-cache, never on a plain cache hit, and never for --archive (a local build was never claimed to come from a release).

The policy is tri-state, and the distinction is deliberate:

OutcomeLauncher
Verifiedprints Provenance verified … and starts
Cannot verify - no gh, not authenticated (gh auth login), no network, API rate limit, or a release older than 0.9.63prints a warning naming the reason and starts on checksum alone
Rejected - the attestation fails the signer policy, or a release from 0.9.63 on has no attestation for the archive's digestrefuses to start (exit 1)

The last row is the case the whole feature exists for. An attacker who can replace a release asset can replace its SHA256SUMS line too, so the checksum still matches - but they cannot forge an attestation. For a release that must have one, "GitHub holds no attestation for this digest" is the tampering signal itself, not missing information.

gh stays optional: the package's contract is zero runtime dependencies, so everything that merely prevents verification warns and continues. To start anyway after a rejection (accepting the risk, e.g. during a GitHub attestation outage): LIBREDB_STUDIO_SKIP_PROVENANCE=1.

Verify by hand at any time:

gh attestation verify ~/.libredb-studio/<version>/libredb-studio-standalone-<version>-<os>-<arch>.tar.gz \
  --repo libredb/libredb-studio \
  --signer-workflow libredb/libredb-studio/.github/workflows/release-artifacts.yml

Homebrew

The formula tracks the latest release (it is rendered and pushed to libredb/homebrew-tap by release CI):

# One-time: Homebrew's untrusted-tap policy requires trusting third-party
# taps. `brew trust` needs Homebrew 6+ - if it prints "Unknown command",
# run `brew update` first (pre-6 Homebrew installs the tap without a trust
# step, but the command below would stop the chain).
brew trust libredb/tap

brew install libredb/tap/libredb-studio

# Foreground (first run prints the generated admin password to the terminal)
libredb-studio

# Or as a background service
brew services start libredb-studio
  • The formula depends on Homebrew's node@24 — the payload's native SQLite storage binding (better-sqlite3) is built against the Node 24 ABI in release CI, so the floating node formula (a newer major) cannot load it — and installs the standalone payload into the keg's libexec; libredb-studio on your PATH runs it.
  • brew services start libredb-studio runs the server on port 3000 with server-side SQLite storage (it sets STORAGE_PROVIDER=sqlite) under $(brew --prefix)/var/libredb-studio/ — the data dir where generated credentials are persisted. The service does not capture stdout, so read the generated password from $(brew --prefix)/var/libredb-studio/auth-bootstrap.json. On Linux, brew services registers a per-user systemd unit (~/.config/systemd/user): it starts at login — not at boot — and stops when your last session ends unless lingering is enabled (loginctl enable-linger $USER).
  • Running libredb-studio directly also defaults STORAGE_SQLITE_PATH to $(brew --prefix)/var/libredb-studio/libredb-storage.db — the same location brew services uses — unless you set it explicitly. This keeps the zero-config auth-bootstrap.json (and any STORAGE_PROVIDER=sqlite data) outside the versioned keg so it survives brew upgrade, and lets both run modes share one data dir.

Configuration

All configuration is environment-driven. The formula does not ship an env file (unlike .deb/.rpm).

Foreground (libredb-studio): export variables in your shell, or prefix the command. Explicit values override zero-config first run. Bind with LIBREDB_BIND (the wrapper maps it to HOSTNAME — see Network exposure):

# AI, auth, OIDC, or Postgres storage — same variables as .env.example
export LLM_PROVIDER=openai LLM_API_KEY=sk-... LLM_MODEL=gpt-4o
# export NEXT_PUBLIC_AUTH_PROVIDER=oidc OIDC_ISSUER=... OIDC_CLIENT_ID=... OIDC_CLIENT_SECRET=...
# export STORAGE_PROVIDER=postgres STORAGE_POSTGRES_URL=postgresql://user:pass@127.0.0.1:5432/libredb
# LIBREDB_BIND=0.0.0.0 libredb-studio   # expose beyond loopback
libredb-studio

brew services: the service block only sets STORAGE_PROVIDER=sqlite, STORAGE_SQLITE_PATH, and HOSTNAME=127.0.0.1. Homebrew has no supported way to inject extra env into that plist (upgrades regenerate it). For LLM, OIDC, Postgres storage, strict auth, or a non-loopback bind, run the binary in the foreground with the env above (or put a reverse proxy in front of the loopback service).

Full variable reference: .env.example. OIDC: docs/OIDC.md. Storage: docs/STORAGE.md.

Linux packages (.deb / .rpm)

Native packages for Debian/Ubuntu and RHEL/Fedora (amd64/x86_64 and arm64/aarch64) are attached to every GitHub release. They bundle the standalone server together with a private, checksum-verified Node.js runtime under /usr/lib/libredb-studio — nothing else to install — and register a hardened systemd service:

VERSION=<version>   # e.g. 0.9.42 - release tags have no v prefix

# Debian / Ubuntu
curl -fsSLO "https://github.com/libredb/libredb-studio/releases/download/${VERSION}/libredb-studio_${VERSION}_amd64.deb"
curl -fsSLO "https://github.com/libredb/libredb-studio/releases/download/${VERSION}/libredb-studio_${VERSION}_amd64.deb.sha256"
sha256sum -c "libredb-studio_${VERSION}_amd64.deb.sha256"
sudo dpkg -i "libredb-studio_${VERSION}_amd64.deb"

# RHEL / Fedora / Rocky
curl -fsSLO "https://github.com/libredb/libredb-studio/releases/download/${VERSION}/libredb-studio-${VERSION}.x86_64.rpm"
curl -fsSLO "https://github.com/libredb/libredb-studio/releases/download/${VERSION}/libredb-studio-${VERSION}.x86_64.rpm.sha256"
sha256sum -c "libredb-studio-${VERSION}.x86_64.rpm.sha256"
sudo rpm -i "libredb-studio-${VERSION}.x86_64.rpm"

Operate it with systemd:

sudo systemctl enable --now libredb-studio   # start now and on boot
journalctl -u libredb-studio                 # first run prints the generated admin password here
sudo systemctl restart libredb-studio        # apply configuration changes
  • Configuration lives in /etc/libredb-studio/env (KEY=value lines, loaded by the unit via EnvironmentFile). The installed file is a commented template covering PORT, HOSTNAME, AUTH_BOOTSTRAP, JWT_SECRET, ADMIN_* / USER_*, and LLM_*. It is mode 0600 (read by systemd as root before privileges drop, so secrets stay away from other users) and marked as a config file — package upgrades never overwrite your edits.
  • State (server-side SQLite storage and generated credentials) lives in /var/lib/libredb-studio, owned by the service's DynamicUser account.
  • The unit runs with systemd hardening (ProtectSystem=strict, NoNewPrivileges, empty capability set, ...) and only writes its state directory.
  • The libredb-studio command (/usr/bin/libredb-studio) can also be run directly without systemd; configuration then comes from your shell environment, and state defaults to ${XDG_STATE_HOME:-~/.local/state}/libredb-studio/ when STORAGE_SQLITE_PATH is unset (the payload directory under /usr/lib is read-only).
  • Removal (apt remove / rpm -e) stops and disables the service; upgrades restart it if it is running (standard systemd maintainer scripts, packaging/linux/scripts/).

Snap

Published on the Snap Store for amd64 and arm64 (live since 0.9.52; release CI publishes every release to the stable channel):

sudo snap install libredb-studio
sudo snap logs libredb-studio   # first run prints the generated admin password here
  • Installing the snap starts a background daemon serving on port 3000.
  • State (SQLite storage, generated credentials) lives in $SNAP_DATA (/var/snap/libredb-studio/current).
  • The snap is strictly confined with only the network / network-bind interfaces: TCP database connections are the supported path; unix-socket connections to databases on the host (e.g. /var/run/postgresql/) are not supported.
  • The .snap file is also attached to each GitHub release for offline installs (sudo snap install --dangerous libredb-studio_<version>_amd64.snap).

Configuration

Unlike the .deb/.rpm packages (which ship /etc/libredb-studio/env), the snap has no dedicated config file. Override environment variables with a systemd drop-in on the unit snapd generates:

sudo systemctl edit snap.libredb-studio.libredb-studio.service

Add [Service] Environment= lines, then restart:

sudo systemctl restart snap.libredb-studio.libredb-studio.service

The drop-in is written to /etc/systemd/system/snap.libredb-studio.libredb-studio.service.d/override.conf (root-owned). Explicit values override the defaults baked into snap/snapcraft.yaml and take precedence over zero-config first run generation. Note that systemctl edit creates the drop-in world-readable (mode 0644, like any systemd override), so after adding secrets tighten it — systemd reads drop-ins as root, so this does not affect the service:

sudo chmod 600 /etc/systemd/system/snap.libredb-studio.libredb-studio.service.d/override.conf

Example drop-in (uncomment and fill what you need):

[Service]
# Bind (default is loopback — see Network exposure above)
#Environment=HOSTNAME=0.0.0.0

# Auth (optional; omit to keep zero-config bootstrap)
#Environment=AUTH_BOOTSTRAP=off
#Environment=JWT_SECRET=change-me-to-a-random-32-char-string
#Environment=ADMIN_EMAIL=admin@libredb.org
#Environment=ADMIN_PASSWORD=

# AI query assistance
#Environment=LLM_PROVIDER=gemini
#Environment=LLM_API_KEY=
#Environment=LLM_MODEL=gemini-2.5-flash
#Environment=LLM_API_URL=http://127.0.0.1:11434/v1

# OIDC (login page reads NEXT_PUBLIC_AUTH_PROVIDER at runtime)
#Environment=NEXT_PUBLIC_AUTH_PROVIDER=oidc
#Environment=OIDC_ISSUER=https://example.auth0.com
#Environment=OIDC_CLIENT_ID=
#Environment=OIDC_CLIENT_SECRET=

# Persisted storage (default: sqlite under $SNAP_DATA — leave unset to keep it)
#Environment=STORAGE_PROVIDER=postgres
#Environment=STORAGE_POSTGRES_URL=postgresql://user:pass@127.0.0.1:5432/libredb?sslmode=disable

Storage: by default the snap sets STORAGE_PROVIDER=sqlite and STORAGE_SQLITE_PATH=$SNAP_DATA/libredb-storage.db — no further config is required. To switch to server-side Postgres, uncomment the STORAGE_PROVIDER / STORAGE_POSTGRES_URL lines above (TCP only; unix-socket Postgres on the host is not reachable under strict confinement).

Full variable reference: .env.example. OIDC setup details: docs/OIDC.md. Storage providers: docs/STORAGE.md.

Windows (winget / Chocolatey / portable zip)

The win32-x64 standalone zip is built and attached to every GitHub release since 0.9.59. winget is live: the first listing (microsoft/winget-pkgs#402985) merged on 2026-07-31, so winget install LibreDB.Studio resolves from the community repository and every release now submits its update PR automatically. The first Chocolatey push is still in community moderation (see the first-listing checklist — track issue #114).

Chocolatey stays switched off in the inventory until its moderation clears (update.ci_enabled: false in distribution/channels.yaml — see Turning a channel's automation off). Chocolatey is the reason the switch exists: push.chocolatey.org answers 403 Forbidden for every version while an account's first submission is unapproved, which failed the 0.9.60 and 0.9.61 release runs even though both releases published correctly. To re-enable: set ci_enabled: true and flip status to live in the same edit — exactly what winget got once its listing merged. The next release publishes the channel; do not re-dispatch release-artifacts for an already-published version to backfill the feed, because its asset steps would fight the immutable release — push a back version by hand (the same choco container the job uses for Chocolatey; the manual manifest recipe for winget).

# winget
winget install LibreDB.Studio

# Chocolatey (after the first push clears community moderation)
choco install libredb-studio

# Then, from any terminal - first run prints the generated admin credentials
libredb-studio

Open http://127.0.0.1:3000 and log in with the printed credentials. Both packages install the same standalone zip: the server payload, a bundled private Node.js runtime (node\node.exe), and the libredb-studio.exe launcher — nothing else to install.

The launcher mirrors the Linux packages' contract:

  • Local-first bind (issue #134): the server binds 127.0.0.1 regardless of any inherited HOSTNAME; set LIBREDB_BIND=0.0.0.0 to expose it on the network.
  • State: server-side SQLite storage and generated first-run credentials live under %LOCALAPPDATA%\LibreDB\Studio\ (created on first run) unless STORAGE_SQLITE_PATH is set, so upgrades and reinstalls never wipe state.
  • Configuration: environment variables only — the same set as Docker and the Linux packages (PORT, LIBREDB_BIND, JWT_SECRET, ADMIN_PASSWORD, LLM_*, OIDC_*, STORAGE_*, ...). See Configuration above and .env.example.

Portable zip (no package manager)

Download libredb-studio-standalone-<version>-win32-x64.zip and SHA256SUMS from the release, verify, extract, run:

# Verify (compare against the SHA256SUMS line for the zip)
Get-FileHash .\libredb-studio-standalone-<version>-win32-x64.zip -Algorithm SHA256

# Extract (Explorer "Extract All...", or - tar -C needs the directory to exist:)
mkdir libredb-studio
tar -xf .\libredb-studio-standalone-<version>-win32-x64.zip -C libredb-studio

# Run from the extracted directory
cd libredb-studio
.\libredb-studio.exe

The zip is flat by design (see Release artifact naming). npx @libredb/studio also works on Windows (Node 20.9+): it downloads this zip, verifies it against SHA256SUMS, and runs the payload with your own Node runtime.

Desktop app (AppImage, Debian package, FlatPark)

Every other channel on this page ships LibreDB Studio as a server you open in a browser. The desktop build ships it as an application window: a Tauri v2 shell starts the same standalone server as a local sidecar on a random loopback port, waits for it to become healthy, signs you in and shows the workspace. There is no browser tab, no port to remember and no password prompt for your own machine. Details of the shell itself are in desktop/README.md (issue #232).

State lives in the per-user data directory - ~/.local/share/org.libredb.Studio for the AppImage, ~/.var/app/org.libredb.Studio/data/org.libredb.Studio under Flatpak - and holds the SQLite storage database plus the generated admin credentials (auth-bootstrap.json, mode 0600). Deleting that directory resets the app.

The session cookie is not marked Secure when a request arrives on a loopback host over plain http, which is what makes the desktop session work: the cookie store behind WebKitGTK discards a Secure cookie delivered over http instead of ignoring the flag the way Chromium does on localhost. Nothing changes for any other deployment - a request on a public host, or one a proxy forwarded with x-forwarded-proto: https, still gets Secure.

AppImage

# Download the AppImage and its checksum sidecar from the release, verify, run
curl -fLO https://github.com/libredb/libredb-studio/releases/download/<version>/libredb-studio-desktop-<version>-linux-x64.AppImage
curl -fLO https://github.com/libredb/libredb-studio/releases/download/<version>/libredb-studio-desktop-<version>-linux-x64.AppImage.sha256
sha256sum -c libredb-studio-desktop-<version>-linux-x64.AppImage.sha256
chmod +x libredb-studio-desktop-<version>-linux-x64.AppImage
./libredb-studio-desktop-<version>-linux-x64.AppImage

The AppImage needs FUSE to mount itself; on a system without it (or in a container), run it with --appimage-extract-and-run. Built for x64 and arm64 against current desktop distributions - the server packages (.deb/.rpm, Snap, Docker) remain the path for headless or older systems.

Debian package (GUI)

curl -fLO https://github.com/libredb/libredb-studio/releases/download/<version>/libredb-studio-desktop_<version>_amd64.deb
curl -fLO https://github.com/libredb/libredb-studio/releases/download/<version>/libredb-studio-desktop_<version>_amd64.deb.sha256
sha256sum -c libredb-studio-desktop_<version>_amd64.deb.sha256
sudo apt install ./libredb-studio-desktop_<version>_amd64.deb

Ships from 0.9.62 for amd64 and arm64. Unlike the AppImage it needs no FUSE, integrates with the desktop menu on install, and takes libwebkit2gtk-4.1-0 and libgtk-3-0 from the distribution rather than bundling them - so it is the smaller, better-behaved choice on a Debian or Ubuntu desktop, and the older-distro caveat that applies to the AppImage applies here too.

This is not the server package. libredb-studio_<version>_<arch>.deb installs a systemd service; this one installs an application. They have different dpkg package names and can be installed side by side.

The bundled Node sidecar is installed as /usr/bin/libredb-studio-node, not /usr/bin/node: that path belongs to the distribution's nodejs package and dpkg refuses to let a second package claim it.

FlatPark (Flatpak)

FlatPark is a signed Flatpak remote and the Flatpak channel for LibreDB Studio, live since 0.9.62 (#241):

flatpak --user remote-add --if-not-exists flatpark https://dl.flatpark.org/flatpark.flatpakrepo
# Flathub is added for the org.gnome.Platform runtime only - the app itself is not published there
flatpak --user remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak --user install flatpark org.libredb.Studio
flatpak run org.libredb.Studio

FlatPark pins the GUI .deb as extra-data: it builds nothing itself, so your own machine downloads that exact release asset at install time and unpacks it inside the sandbox. It does not accept AppImages at all - its runtime has no libfuse.

The Flatpak sandbox has network access and no filesystem access. Databases reachable over TCP - including a database on the host at 127.0.0.1 - work out of the box; opening local SQLite files or connecting through a Unix socket needs an explicit grant, the same trade-off the strictly confined Snap makes:

# Local Postgres over its unix socket
flatpak override --user --filesystem=/run/postgresql:ro org.libredb.Studio
# Local MySQL/MariaDB over its unix socket
flatpak override --user --filesystem=/var/run/mysqld:ro org.libredb.Studio
# A directory of SQLite database files
flatpak override --user --filesystem=~/databases org.libredb.Studio
# Revert every override
flatpak override --user --reset org.libredb.Studio

The descriptor set, the local build flow and the submission checklist live in packaging/flatpark/README.md.

Flathub (submission declined)

LibreDB Studio is not on Flathub, and flatpak install flathub org.libredb.Studio will not work. Install from the FlatPark remote above - same app id, same sandbox policy, same flatpak override commands.

The submission (flathub/flathub#9538) was declined on 2026-07-30 under Flathub's generative AI policy. The reviewer's position was that developing with an AI assistant disqualifies the app, irrespective of who designed, reviewed and released it. We closed the PR ourselves rather than argue the point; the closing comment leaves the verifiable maintenance record on the thread.

packaging/flatpak/ stays in the tree and stays honest: the manifest still renders, and flatpak-smoke.yml still builds the AppImage and repacks it whenever those paths change. But a resubmission would also have to be built from source - repacking a released AppImage is a second, independent blocker under Flathub's requirements - so treat that directory as dormant rather than as pending work (#232).

Building and running either Flatpak locally

Neither channel needs a published release to test. Both take a locally built artifact:

# FlatPark: build the GUI .deb, then the Flatpak that pins it.
# --deb-only skips the AppImage, so the linuxdeploy GTK toolchain (librsvg2-dev
# and friends) is not needed.
gh release download <version> --repo libredb/libredb-studio \
  --pattern "libredb-studio-standalone-<version>-linux-x64.tar.gz"
bash scripts/build-desktop-appimage.sh dist-desktop \
  --payload libredb-studio-standalone-<version>-linux-x64.tar.gz --deb-only --smoke
bash scripts/build-flatpark-local.sh \
  dist-desktop/libredb-studio-desktop_<version>_amd64.deb --install
flatpak run org.libredb.Studio//stable

# packaging/flatpak (dormant, see above): build the AppImage, then the Flatpak that repacks it.
bash scripts/build-desktop-appimage.sh dist-desktop --payload <tarball> --smoke
bash scripts/build-flatpak-local.sh \
  dist-desktop/libredb-studio-desktop-<version>-linux-x64.AppImage --install
flatpak run org.libredb.Studio

Drop --payload to build the payload from the working tree instead of a released tarball - slower, but it tests the code you actually have.

Both need the builder and the runtime the manifests declare:

flatpak install -y flathub org.flatpak.Builder org.gnome.Platform//50 org.gnome.Sdk//50

The FlatPark script builds on branch stable, so its ref does not collide with a Flathub-style local build of the same app id. Note that both share ~/.var/app/org.libredb.Studio/ - the app id is the same - so deleting that directory resets connections and query history for whichever builds you have installed. To remove a local FlatPark build:

flatpak --user uninstall -y org.libredb.Studio//stable
flatpak --user remote-delete libredb-flatpark-local

Building a standalone payload locally

The single source of truth for the release archives also works locally (Linux and macOS; on Windows it runs under Git Bash and produces the flat zip — the release job then adds the bundled Node runtime and launcher, see packaging/windows/README.md):

bun install
bash scripts/build-standalone-payload.sh dist --smoke
# -> dist/libredb-studio-standalone-<version>-<os>-<arch>.tar.gz

--smoke boots the packed payload with node server.js and requires GET /api/db/health to return 200. Run the result via the npx launcher: npx @libredb/studio --archive dist/libredb-studio-standalone-<version>-<os>-<arch>.tar.gz.

Maintainer notes

Release publishing is driven by two workflows, both triggered on release: published (plus workflow_dispatch):

Embedded-sample channel E2E

Every zero-config channel must prove — in a real browser — that a fresh install serves the embedded sample connections ("Sample (LibreDB)", "Sample (Employees)"). One bun run test:e2e is not enough: each channel boots the payload through a different launcher, cwd, and STORAGE_SQLITE_PATH, so a packaging bug (a payload missing seed-assets/, a dropped fileset entry) surfaces only in the affected channel.

The shared spec is e2e/embedded-samples.spec.ts; playwright.channel.config.ts runs it against an externally booted server (CHANNEL_E2E_BASE_URL, no webServer). The orchestrator boots one channel, waits for health (and best-effort for the async seed), runs Playwright, and tears down:

scripts/channel-embedded-sample-e2e.sh <channel> [artifact]
scripts/channel-embedded-sample-e2e.sh all            # every channel feasible here

# channels: tarball | npx | docker | deb | rpm | snap | homebrew
scripts/channel-embedded-sample-e2e.sh tarball dist/libredb-studio-standalone-*.tar.gz
scripts/channel-embedded-sample-e2e.sh docker ghcr.io/libredb/libredb-studio:main

all skips infeasible channels (no docker daemon, no passwordless sudo, not macOS, missing artifact) and prints a summary. Where each channel runs in CI:

ChannelCI gate
next-devci.yml e2e job (regular Playwright run includes the spec)
tarball, npxci.yml channel-e2e job (payload artifact)
dockerdocker-build-push.yml channel-e2e job (pushed image; gates the Helm release dispatch)
deb, rpmrelease-artifacts.yml linux-packages job (amd64; alongside the zero-config smoke)
snaprelease-artifacts.yml snap job (amd64; runs before the store publish)
homebrewnot in CI (linux runners) — run locally on macOS

First-release validation runbook

All channels have now had their first live run (the Snap publish completed its first with 0.9.52, validated per this runbook). Right after publishing a release:

  1. Watch the release-artifacts run: all four tarball legs green (if the macos-15-intel or macos-14 runner labels ever disappear, check the current labels in actions/runner-images - do not fall back to retired macos-13 or paid -large labels blindly), .deb/.rpm uploaded with .sha256 sidecars, SHA256SUMS complete, tap push and snap jobs behaving per their secrets.
  2. npx @libredb/studio@<version> on a clean machine: download + checksum + first-run banner + login. The npx Engine Smoke workflow (npx-engine-smoke.yml) runs automatically after a successful NPM Publish: it waits for the registry to serve the released version, then runs bare npx @libredb/studio on Node 20.9/22/24 and asserts each tier resolves exactly that release (the #130 regression class - npm's picker avoids engine-incompatible versions for bare specs). Check that it went green; dispatch it manually to re-run.
  3. brew tap libredb/tap && brew install libredb-studio && brew services start libredb-studio.
  4. Download the .deb on Debian/Ubuntu: dpkg -i, systemctl start libredb-studio, health 200 on 127.0.0.1:3000; verify the arm64 package on an arm64 machine (the CI smoke covers amd64 only; the bundled node arch is statically asserted for both).
  5. Spot-check provenance on one downloaded asset and the image - a green release job proves the attestation step ran, not that the published asset is the one it covers: gh attestation verify <asset> --repo libredb/libredb-studio and gh attestation verify oci://ghcr.io/libredb/libredb-studio:<version> --repo libredb/libredb-studio, plus npm audit signatures after installing the package (see Verifying a release artifact).

Artifact provenance roadmap

Standalone tarballs and .deb/.rpm packages are checksum-verified against SHA256SUMS / per-package .sha256 sidecars (see Release artifact naming) — both from the same GitHub release. That pairing detects corruption, not substitution: whoever can replace an asset can replace its checksum line too. The .snap release asset ships no sidecar (--dangerous installs skip the Snap Store's own verification).

Signed provenance moves the trust root out of the release — the signer is the workflow's GitHub OIDC identity (repo + workflow + commit), recorded in a public transparency log. All three steps of issue #123 landed for 0.9.63:

StepScopeState
npm provenancenpm tarball (npm publish --provenance, Sigstore bundle held by npmjs)done (0.9.63) — verify with npm audit signatures
SLSA build provenancestandalone tarballs, win32 zip, .deb/.rpm, desktop AppImage + GUI .deb, .snap, GHCR image (actions/attest-build-provenance)done (0.9.63) — verify with gh attestation verify
Launcher-side verificationbin/studio.js checks the downloaded archive's attestation when gh is presentdone (0.9.63) — see Launcher provenance check

No step needed a new secret: the attestations live in GitHub's attestation store, not in the release, so nothing an attacker can reach by replacing a release asset also lets them forge a signature.

Verifying a release artifact

# Any release asset (tarball, zip, .deb, .rpm, .AppImage, .snap)
gh attestation verify libredb-studio-standalone-0.9.63-linux-x64.tar.gz \
  --repo libredb/libredb-studio

# The container image, by digest or by tag
gh attestation verify oci://ghcr.io/libredb/libredb-studio:0.9.63 \
  --repo libredb/libredb-studio

A pass prints the workflow and commit that produced the artifact. gh attestation verify reaches GitHub for the bundle, so it needs network access; --bundle verifies a previously downloaded one offline.

Notes on what is and is not covered:

  • The .snap attestation only exists when the release actually built one (the job is gated on SNAPCRAFT_STORE_CREDENTIALS); Snap Store installs are verified by the store instead.
  • SHA256SUMS and the .sha256 sidecars are deliberately not attested — a signature over a checksum file proves nothing about the artifact it describes. The artifacts are the subjects.
  • Images built from branch pushes (the mutable main / dev tags) are not attested; only release-context builds are.
  • The image attestation is not pushed to GHCR as an OCI referrer, so cosign-style registry-only verification will not find it — fetch it from GitHub with gh attestation verify oci://….
  • Buildx's own inline provenance on the manifest is unrelated and unsigned; it is left at the action's defaults on purpose (changing it rewrites the manifest structure, which the Docker Hub mirror's immutable-tag rules are sensitive to).

Failure modes to expect on release day: npm publish --provenance hard-fails if the job lacks id-token: write or if Sigstore is unreachable, and actions/attest-build-provenance fails the same way — with a job-level attestations: write missing, or if the GitHub attestation API is down. A failed npm publish cannot be retried on the same tag (follow the usual rule and cut the next patch version); a failed attestation happens before gh release upload in the same job, so re-running that job is safe — the release is still a draft at that point.

CI secrets that gate publishing

Optional-channel steps are skipped cleanly when their secret is absent, so forks and partial setups still publish the rest:

SecretGatesWithout it
TAP_GITHUB_TOKENRendering and pushing the Homebrew formula to libredb/homebrew-tap (needs write access to that repo)Tap update skipped; tarballs still attach to the release
SNAPCRAFT_STORE_CREDENTIALSThe entire snap build/publish job (exported via snapcraft export-login)Snap job skipped
DOCKER_HUB_TOKEN (+ DOCKER_HUB_USERNAME variable)The Docker Hub mirror pushGHCR-only publish
CHOCO_API_KEYThe chocolatey job: choco pack + choco push to https://push.chocolatey.org/ (API key of the libredb community account)Chocolatey publish skipped; the win32 zip still attaches to the release
Every row above whose channel is switchable also needs update.ci_enabled: true in distribution/channels.yaml: the secret says CI can publish, the flag says it should. See Turning a channel's automation offChannel skipped with a notice; the release publishes normally
WINGETCREATE_GITHUB_TOKENThe winget job: wingetcreate update --submit PRs to microsoft/winget-pkgs. Classic PAT with public_repo scope — wingetcreate does not support fine-grained PATswinget submission skipped

The chocolatey and winget jobs run strictly after publish-release: both channels download the zip from the release URL, which is public only once the release is published. A failure there never unpublishes or blocks the release (same trust model as the npm/Docker dispatch chain) — but it does make the workflow run report failure overall, which reads as a failed release when it was not. That is why a channel known to be unable to publish is switched off in the inventory rather than left to fail: the run's colour has to mean something.

By contrast, the windows-package job (which builds the win32 zip) is a hard release gate, exactly like the POSIX build matrix: the zip is on the publish-release required-assets list, so a Windows build failure blocks the whole release train by design — a release without its Windows artifact would strand winget/Chocolatey (their manifests point at that exact asset) and npx on win32.

Windows first-listing checklist

The win32 zip has shipped in every release since 0.9.59. The release automation for winget and Chocolatey is secret-gated and ready — both CHOCO_API_KEY (API key of the libredb account on community.chocolatey.org) and WINGETCREATE_GITHUB_TOKEN are configured in the repo's Actions secrets. The FIRST listing in each community catalog is a one-time human step (same pattern as the Snap Store name registration). Both first submissions were made with 0.9.59: winget merged on 2026-07-31 and is live with ci_enabled: true, while the Chocolatey push is still in moderation.

  1. Chocolatey — the release's chocolatey job packs and pushes automatically. The first push enters human moderation; while a version sits in the moderation queue, pushing further versions can be rejected — if a release-time push fails during that window, re-run the job after approval.

  2. winget — DONE: microsoft/winget-pkgs#402985 (the Microsoft CLA is signed on that first PR). The first manifest could not be submitted by wingetcreate update — the package id did not exist yet, and the release job detects that and skips with a notice — so it was rendered from the in-repo templates and opened by hand. Later releases submit update PRs automatically; the same manual recipe still covers a back version (a release published while the channel was switched off, since re-dispatching release-artifacts for a published version is not an option):

    VERSION=<version>
    gh release download "$VERSION" --pattern SHA256SUMS --dir dist
    for t in packaging/winget/*.tmpl; do
      out="manifests/l/LibreDB/Studio/$VERSION/$(basename "${t%.tmpl}")"
      node scripts/render-windows-packaging.mjs "$t" dist/SHA256SUMS "$VERSION" "$out"
    done
    

    Then either wingetcreate submit --token <classic-PAT> manifests/l/LibreDB/Studio/$VERSION (Windows only — wingetcreate ships a win-x64 binary), or, from any OS, commit the three files to a branch on a microsoft/winget-pkgs fork and open the PR with gh — the whole submission is "add manifests/l/LibreDB/Studio/<version>/", nothing else changes. Title convention: New package: <id> version <version> for a first listing, New version: <id> version <version> for an update. Upstream's validation pipeline downloads the InstallerUrl and verifies InstallerSha256, so the release must already be published.

  3. After each catalog goes live: flip its distribution/channels.yaml entry to status: live, set links.first_pr, and add a measurable pin where one exists (Chocolatey community API). winget is measured by a probe, not a regex pin (pin.strategy: probe, winget-max-version): it publishes no floating "latest" document — the winget-pkgs contents listing enumerates every published version — so the checker takes the highest version enumerated there. A regex pin cannot express that, because it requires all matches in a source to agree.

Channel inventory and drift check

For the coverage matrix (live counts by category and platform, full channel table), see docs/CHANNELS.md. Regenerate it with bun run distribution:matrix after editing the inventory; bun run distribution:matrix --check fails when the generated regions are stale.

distribution/channels.yaml is the machine-readable inventory of every distribution channel: identity, business category, update policy, provenance links, and (where measurable) where its pinned version lives. bun run distribution:check (scripts/distribution-check.mjs) compares every live channel's pin against package.json and prints a markdown drift table; the weekly distribution-check.yml workflow writes the same table to its Job Summary (cron + manual dispatch — deliberately not release: published, which GITHUB_TOKEN-published releases never fire). The checker only reads the inventory; bumping a pin or editing a channel entry is always a human commit.

Tiers describe who controls publication:

TierMeaningExamples
0Core registries, published directly by release CIGitHub Releases, GHCR, Docker Hub, npm
1Packaged formats owned by this repo, CI-publishedHelm, Homebrew tap, Snap, .deb/.rpm, desktop AppImage
2LibreDB-owned copies and listings, bumped by handCapRover mirror (deprecated), Railway, Koyeb button, Fly.io config, Render Blueprint
3Upstream community catalogs, bumped via PRCapRover official, Dokploy, Cosmos, Kubero
4Partner or curated catalogs (not self-serve)Rancher partner charts, Koyeb catalog, DO, winget, Chocolatey, Flathub

Categories (category on every channel) are the business-facing buckets rendered in docs/CHANNELS.md: registries-releases, containers, kubernetes-operators, package-managers, os-desktop, paas-catalogs, deploy-recipes, cloud-marketplaces. They are independent of tier (who publishes). paas-catalogs and deploy-recipes look similar but answer different questions: paas-catalogs means the platform itself lists LibreDB Studio in its own catalog, so a user browsing that platform discovers it without visiting this repo; deploy-recipes means we publish a config file or instructions and the platform does not list us anywhere, so discovery only happens through our repo. There is no closed category: a declined or retired channel is status: deprecated with its category unchanged (Flathub is category: package-managers, status: deprecated) — category describes what the channel technically is, status describes its lifecycle, and an earlier revision that conflated the two (Flathub filed under a closed category) is what produced a false coverage claim in the scorecard.

Kind (kind on every channel) is the technical shape of the artefact — a Helm chart, a container image, a curated marketplace listing — and is validated against a fixed enum in scripts/distribution-check.mjs (CHANNEL_KINDS). It is independent of category: kubernetes-operators (category) spans helm-chart, operator-catalog and partner-catalog (kind), and paas-template (kind) spans both paas-catalogs and deploy-recipes (category). Neither axis determines the other, so both are kept and validated separately rather than collapsed into one.

Platforms (platforms on every channel, at least one) are the user-facing axis rendered in docs/CHANNELS.md: linux, macos, windows, container, kubernetes, cloud. They are independent of both tier (who publishes) and category (which business bucket). A channel may list several; the matrix always renders them in that canonical order.

SLAs (update.sla) state how quickly a channel is expected to follow a release: every_release (bumped as part of releasing), minor_plus (bumped for minor releases and above), major_only, on_demand (bumped when someone gets to it — the honest default for PaaS catalog templates).

Pin strategies: local_file (version lives in files in this repo), remote_file (fetched from an upstream raw URL, best-effort — fetch failures degrade to UNKNOWN and never fail the run), probe (see below), none (nothing to measure, stated explicitly with a note). The two regex strategies carry an extract pattern with exactly one capture group; when a source yields multiple different matches the channel is reported instead of silently using the first hit.

Probes (#182) measure the channels whose served state is not one document a single regex can read. Each names a probe and the urls it needs; the URLs live in the inventory rather than in the script so the unit tests can point them at a local server and the suite never touches the real network. Probe failures degrade to UNKNOWN exactly like a failed remote_file fetch — but a tag that is genuinely absent is DRIFT, not UNKNOWN, which is the whole point: a publish step that skipped silently must not read as "could not measure".

ProbeChannelWhat it measures
ghcr-tag-digestdocker-ghcrthat the digest :latest resolves to equals the released version tag's (anonymous pull token — no secret, works from a fork)
dockerhub-tag-digestdocker-hub-mirrorthe same digest equality on the mirror; an absent version tag is how an expired DOCKER_HUB_TOKEN (whose push step skips silently) becomes visible
snap-store-channelsnapthe stable version of every listed architecture, each as its own source, so one lagging build is drift rather than a pass; edge may legitimately differ and is ignored
winget-max-versionwingetthe highest version the catalog enumerates, because winget publishes no floating "latest" document

Because a probe resolves versions in code, a channel measured this way needs no extract. A probe that reports several sources (Snap's architectures) reuses the same rule as a multi-file local_file pin: disagreeing sources are drift, with every source shown.

Strict mode: bun run distribution:check --strict exits non-zero only for local_file channels with sla: every_release that are drifted or unmeasurable. Remote catalogs and on_demand templates never gate, so strict is enableable today without first paying off historical PaaS drift. For the Helm chart the enforcement remains the required chart:check CI gate (#138) — the matrix row is visibility, not a second gate. --json emits the rows for scripting.

Turning a channel's automation off

Some channels can be temporarily unable to publish for reasons outside this repository — a package waiting in community moderation, a listing still under review. Left to fail they make a published release report failure, which is worse than not publishing: the run's colour stops meaning anything. update.ci_enabled in channels.yaml is the switch:

  - id: chocolatey
    update:
      method: ci_publish
      sla: every_release
      ci_enabled: false   # 403 for every version while the first push is in moderation

The release workflow reads it (distribution-check.mjs --ci-outputs in the channels job, whose outputs each channel's availability step consults), so the edit is the whole switch — no secret to delete, no workflow change, and the decision is reviewable in a diff next to the channel's status and note. A channel is published only when its flag says true and its secret is present.

Three deliberate constraints:

  • Only optional channels may carry the flag: docker-hub-mirror, homebrew, snap, winget, chocolatey. The core release path (github-release, docker-ghcr, npm, helm) and the assets publish-release requires (.deb/.rpm, AppImage, the win32 zip) have no switch, because one mistyped false there would silently ship a release with no npm package or no image. parseChannels rejects the flag anywhere else, so this is enforced, not just documented.
  • It is required, never defaulted, on those five: CI behaviour for a channel has to be a stated decision in this file, not an omission.
  • Failure leaves channels off, never the release blocked. The channels job is continue-on-error; if it cannot run, its outputs are empty, every gate reads "not true", and the optional channels skip while the release publishes normally. A forgotten re-enable surfaces in the weekly drift table rather than in a broken release.

Do not confuse it with status: status describes the channel's listing (chocolatey is pending because its first push has not cleared moderation, while the zip it points at is built and required on every release), whereas ci_enabled describes only whether release CI may publish it. The two move together when a listing lands — winget went pending/false to live/true in a single edit once #402985 merged. A channel whose listing will never happen is deprecated, not pendingflathub is the worked example.

Adding a channel = one new entry in channels.yaml (copy a neighbour of the same tier; the schema is validated on every run). Set links.first_pr to the PR that landed the listing, and update links.last_bump_pr whenever a version-bump PR for that channel merges — it is null until the first post-listing bump and is displayed, not auto-discovered.

Root-level PaaS configs. Two tier-2 channels ship their descriptor at the repo root rather than under deploy/<provider>/: fly.toml (Fly.io) and render.yaml (Render). This is deliberate, not a stray file — their tooling auto-detects the config at the working-directory root: fly launch/fly deploy read ./fly.toml (see docs/FLY.md) and Render's Blueprint auto-detects render.yaml, so the documented "clone and deploy" flow only works from that location. Catalog-based channels (CapRover, Railway, Dokploy, Kubero, …) instead keep their source descriptor under deploy/<provider>/, because the consumable artifact lives in an external catalog and the in-repo file is only the source that gets pushed or PR'd upstream. Neither Fly.io nor Render has a marketplace or template gallery to publish into, which is why the repo file itself is the deliverable (pin.strategy: local_file for the version-pinned fly.toml; none for render.yaml, which builds from the repo Dockerfile and tracks whatever main builds).

Manual steps still open

  • Operator first listings: the controller image is done — ghcr.io/libredb/libredb-studio-operator:0.9.59 was built once by manual workflow_dispatch from the post-merge main commit (the 0.9.59 tag carries neither operator/ nor the workflow file, and GitHub only dispatches workflows that exist on the chosen ref) and the GHCR package is public, which community catalog CI requires. From 0.9.60 on the tag ref carries the operator and the normal tag-pinned dispatch chain applies. What is still open is upstream: the operatorhub.io bundle PR (k8s-operatorhub/community-operators#8794) and the OpenShift catalog PR (community-operators-prod#10581, the second half of the FBC release whose bundle merged as #10497) both wait on maintainer review. Flip operatorhub-community in distribution/channels.yaml from pending to live once the listings are visible, and remember release-config.yaml for every later release (see An FBC release is two upstream PRs).
  • Snap Store listing screenshots: the description and icon ship with the snap (snap/snapcraft.yaml, public/logo.svg), but screenshots are a manual upload in the Snap Store web UI (https://snapcraft.io/libredb-studio/listing). The snap name is registered and SNAPCRAFT_STORE_CREDENTIALS is configured — the channel went live with 0.9.52.
  • Website install docs: the libredb-website documentation must be updated with the new channels (npx, Homebrew, .deb/.rpm, Snap) — a cross-repo step and part of issue #111's "README and website docs" acceptance criterion (and implicitly of #110/#112/#113).
  • Windows first listings: winget is done — microsoft/winget-pkgs#402985 merged on 2026-07-31 and the channel is live with release-CI updates enabled. Only the Chocolatey first push is still outstanding (human moderation), per the Windows first-listing checklist — tracked in issue #114.
  • Flathub submission — closed, not shipping. flathub/flathub#9538 was declined on 2026-07-30 under Flathub's generative AI policy and closed from our side; see Flathub (submission declined). The Flatpak channel is FlatPark, live since 0.9.62. The domain-verification file at https://libredb.org/.well-known/org.flathub.VerifiedApps.txt is served but carries no token, since Flathub only issues one after publication. Tracked in issue #232.
  • Desktop app, remaining channels: the Tauri v2 wrapper now ships as an AppImage on Linux (see Desktop app). The macOS .dmg plus brew cask, the Microsoft Store MSIX and the Tauri updater are still open and need paid signing identities — see docs/DESKTOP_WRAPPER_SPIKE.md.

Issue close-out notes

Deviations and partial deliveries to record on the tracking issues when closing them, so the record matches the implementation:

  • #110 (npx): the "Works on Linux, macOS, and Windows" acceptance criterion was initially NOT delivered for Windows (the launcher exited on win32 with a Docker pointer); the win32 path shipped with #114 — the npx launcher now downloads the win32 zip and runs it with the user's Node runtime.
  • #111 (Homebrew): the website half of "Install instructions added to README and website docs" is a cross-repo step (see Manual steps above).
  • #113 (Snap): closed after the 0.9.52 live validation (store publish from release CI, amd64+arm64 on stable). Listing screenshots remain a store-side manual upload (see Manual steps above); version-bump auto-refresh is observable at the next release.
  • #115 (desktop wrapper spike): the written go/no-go recommendation is delivered (docs/DESKTOP_WRAPPER_SPIKE.md), but the hands-on spike scope (Tauri prototype, WebKitGTK/Monaco validation, unsigned PoC builds) was re-scoped into its Phase 1 — close as "recommendation delivered, hands-on spike open" and open the Phase 1 follow-up issue.
  • #118 (Helm AUTH_BOOTSTRAP): the chart originally defaulted to strict mode (config.authBootstrap: "off"), deviating from the issue's "default to the app default (on)" wording — generated credentials in centrally collected pod logs are undesirable. This was later reversed for the Rancher partner-charts certification, whose repository requires charts to be deployable with default values: the chart now defaults to "" (zero-config bootstrap), and strict mode remains available via config.authBootstrap=off.