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.
| Channel | Best for | Section |
|---|---|---|
| Docker | Servers, PaaS, CI, quickest start | Docker |
| Helm | Kubernetes | Helm (Kubernetes) |
| npx | Trying it on a laptop with Node installed | npx |
| Homebrew | macOS / Linux workstations | Homebrew |
| .deb / .rpm | Debian/Ubuntu and RHEL/Fedora servers (systemd) | Linux packages (.deb / .rpm) |
| Snap | Ubuntu and other snapd systems | Snap |
| Windows (winget / Chocolatey / portable zip) | Windows workstations | Windows |
| Desktop app (AppImage, .deb, FlatPark) | Linux desktops - an application window, no browser tab | Desktop 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 isnode:24.16.0-trixie-slimandCMDexecsnode 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:sqliteunder Bun,node:sqliteunder 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./datainside 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.)
| Channel | Default bind | How to expose |
|---|---|---|
| npx | 127.0.0.1 | npx @libredb/studio --host 0.0.0.0 (or set HOSTNAME) |
| .deb / .rpm (systemd) | 127.0.0.1 | HOSTNAME=0.0.0.0 in /etc/libredb-studio/env, then restart |
| .deb / .rpm (direct run) | 127.0.0.1 | LIBREDB_BIND=0.0.0.0 libredb-studio |
| Homebrew service | 127.0.0.1 | run the binary manually with LIBREDB_BIND=0.0.0.0, or front it with a reverse proxy |
| Snap | 127.0.0.1 | sudo systemctl edit snap.libredb-studio.libredb-studio.service with [Service] Environment=HOSTNAME=0.0.0.0 |
| Docker / Helm | 0.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:
| Artifact | Name | Targets |
|---|---|---|
| Standalone server tarball | libredb-studio-standalone-<version>-<os>-<arch>.tar.gz | linux-x64, linux-arm64, darwin-x64, darwin-arm64 |
| Standalone server zip (Windows) | libredb-studio-standalone-<version>-win32-x64.zip | win32-x64 (bundled Node runtime + libredb-studio.exe launcher) |
| Checksums | SHA256SUMS | covers all standalone tarballs and the win32 zip |
| Debian package | libredb-studio_<version>_<arch>.deb (+ .sha256 sidecar) | amd64, arm64 |
| RPM package | libredb-studio-<version>.<arch>.rpm (+ .sha256 sidecar) | x86_64, aarch64 |
| Snap | libredb-studio_<version>_<arch>.snap | amd64, arm64 (also published to the Snap Store) |
| Desktop AppImage | libredb-studio-desktop-<version>-linux-<arch>.AppImage (+ .sha256 sidecar) | x64, arm64 (also the artifact the in-repo Flatpak manifest repacks) |
| Desktop Debian package | libredb-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:
| Tag | Published from | Mutability |
|---|---|---|
<version> (e.g. 0.9.41) | GitHub release (or manual dispatch) only | pinned, never overwritten by branch pushes |
latest | GitHub release only | moves on each release |
main | every push to main (including PR merges) | moving pre-release tag |
dev | every push to a feat/** / fix/** branch | moving development tag |
sha-<commit> | every build | immutable |
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.ymlbuilds and pushesghcr.io/libredb/libredb-studio-operator:<version>(amd64+arm64) on every app release. Chart releases (libredb-studio-x.y.ztags) are skipped quietly. The embedded chart's declared dependencies are vendored withhelm dependency buildright before the image build;charts/*.tgzis never committed. - Catalog bundle —
operator/bundle/(generated bymake -C operator bundle, which stamps the versionedcontainerImageCSV annotation and validates with--select-optional suite=operatorframework— the same suite the community pipelines run; no validator flags a missingcontainerImage, 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: trueplus a per-OCP-versioncatalog_mappingin the submission'sci.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, defaultreview-needed). Channeloperatorhub-communityindistribution/channels.yamltracks it (status: pendinguntil 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:
-
Bundle PR —
operators/libredb-studio-operator/<version>/(a copy ofoperator/bundle) plusci.yaml,Makefileandcatalog-templates/. Merging it runs the release pipeline, which publishesquay.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. -
Catalog PR — the rendered
catalogs/v4.15..v4.22/libredb-studio-operator/catalog.yamlfiles, one per OCP version in thecatalog_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 versionThe renderer applies
--migrate-level bundle-object-to-csv-metadataonly for v4.17+, which is why the pre-4.17 catalogs keep the base64olm.bundle.objectform 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):
| Node | Support |
|---|---|
| 24+ (recommended) | Everything works |
| 22.13 - 23.x | Works; 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.12 | Works, minus all SQLite features: SQLite database connections need the built-in node:sqlite (unflagged from Node 22.13). |
| < 20.9 | Refused 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 withscripts/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 --provenanceinnpm-publish.yml, from 0.9.63). npmjs stores a Sigstore attestation naming the repo, workflow and commit that built the tarball, sonpm audit signaturesverifies 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:
| Outcome | Launcher |
|---|---|
| Verified | prints 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.63 | prints 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 digest | refuses 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 floatingnodeformula (a newer major) cannot load it — and installs the standalone payload into the keg'slibexec;libredb-studioon your PATH runs it. brew services start libredb-studioruns the server on port 3000 with server-side SQLite storage (it setsSTORAGE_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 servicesregisters 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-studiodirectly also defaultsSTORAGE_SQLITE_PATHto$(brew --prefix)/var/libredb-studio/libredb-storage.db— the same locationbrew servicesuses — unless you set it explicitly. This keeps the zero-configauth-bootstrap.json(and anySTORAGE_PROVIDER=sqlitedata) outside the versioned keg so it survivesbrew 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=valuelines, loaded by the unit viaEnvironmentFile). The installed file is a commented template coveringPORT,HOSTNAME,AUTH_BOOTSTRAP,JWT_SECRET,ADMIN_*/USER_*, andLLM_*. 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'sDynamicUseraccount. - The unit runs with systemd hardening (
ProtectSystem=strict,NoNewPrivileges, empty capability set, ...) and only writes its state directory. - The
libredb-studiocommand (/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/whenSTORAGE_SQLITE_PATHis unset (the payload directory under/usr/libis 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-bindinterfaces: TCP database connections are the supported path; unix-socket connections to databases on the host (e.g./var/run/postgresql/) are not supported. - The
.snapfile 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: falseindistribution/channels.yaml— see Turning a channel's automation off). Chocolatey is the reason the switch exists:push.chocolatey.organswers403 Forbiddenfor 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: setci_enabled: trueand flipstatustolivein the same edit — exactly what winget got once its listing merged. The next release publishes the channel; do not re-dispatchrelease-artifactsfor 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.1regardless of any inheritedHOSTNAME; setLIBREDB_BIND=0.0.0.0to expose it on the network. - State: server-side SQLite storage and generated first-run credentials live under
%LOCALAPPDATA%\LibreDB\Studio\(created on first run) unlessSTORAGE_SQLITE_PATHis 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):
docker-build-push.yml— GHCR image (version +latesttags) and the Docker Hub mirror.release-artifacts.yml— standalone tarballs + the win32 zip +SHA256SUMS,.deb/.rpm(nfpm,packaging/linux/), the Homebrew formula (packaging/homebrew/rendered byscripts/render-homebrew-formula.mjs), the snap (snap/snapcraft.yaml), and — after the release publishes — the Chocolatey push and winget update PR (packaging/chocolatey/andpackaging/winget/rendered byscripts/render-windows-packaging.mjs).- The npm package (
@libredb/studio, which carries the npx launcherbin/studio.js) is published by the separate npm-publish workflow, also onrelease: published.
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:
| Channel | CI gate |
|---|---|
| next-dev | ci.yml e2e job (regular Playwright run includes the spec) |
| tarball, npx | ci.yml channel-e2e job (payload artifact) |
| docker | docker-build-push.yml channel-e2e job (pushed image; gates the Helm release dispatch) |
| deb, rpm | release-artifacts.yml linux-packages job (amd64; alongside the zero-config smoke) |
| snap | release-artifacts.yml snap job (amd64; runs before the store publish) |
| homebrew | not 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:
- Watch the
release-artifactsrun: all four tarball legs green (if themacos-15-intelormacos-14runner labels ever disappear, check the current labels in actions/runner-images - do not fall back to retiredmacos-13or paid-largelabels blindly),.deb/.rpmuploaded with.sha256sidecars,SHA256SUMScomplete, tap push and snap jobs behaving per their secrets. npx @libredb/studio@<version>on a clean machine: download + checksum + first-run banner + login. Thenpx Engine Smokeworkflow (npx-engine-smoke.yml) runs automatically after a successful NPM Publish: it waits for the registry to serve the released version, then runs barenpx @libredb/studioon 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.brew tap libredb/tap && brew install libredb-studio && brew services start libredb-studio.- Download the
.debon Debian/Ubuntu:dpkg -i,systemctl start libredb-studio, health 200 on127.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). - 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-studioandgh attestation verify oci://ghcr.io/libredb/libredb-studio:<version> --repo libredb/libredb-studio, plusnpm audit signaturesafter 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:
| Step | Scope | State |
|---|---|---|
| npm provenance | npm tarball (npm publish --provenance, Sigstore bundle held by npmjs) | done (0.9.63) — verify with npm audit signatures |
| SLSA build provenance | standalone 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 verification | bin/studio.js checks the downloaded archive's attestation when gh is present | done (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
.snapattestation only exists when the release actually built one (the job is gated onSNAPCRAFT_STORE_CREDENTIALS); Snap Store installs are verified by the store instead. SHA256SUMSand the.sha256sidecars 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/devtags) 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 withgh 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:
| Secret | Gates | Without it |
|---|---|---|
TAP_GITHUB_TOKEN | Rendering 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_CREDENTIALS | The entire snap build/publish job (exported via snapcraft export-login) | Snap job skipped |
DOCKER_HUB_TOKEN (+ DOCKER_HUB_USERNAME variable) | The Docker Hub mirror push | GHCR-only publish |
CHOCO_API_KEY | The 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 off | Channel skipped with a notice; the release publishes normally |
WINGETCREATE_GITHUB_TOKEN | The winget job: wingetcreate update --submit PRs to microsoft/winget-pkgs. Classic PAT with public_repo scope — wingetcreate does not support fine-grained PATs | winget 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.
-
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.
-
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-dispatchingrelease-artifactsfor 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" doneThen 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 amicrosoft/winget-pkgsfork and open the PR withgh— the whole submission is "addmanifests/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 theInstallerUrland verifiesInstallerSha256, so the release must already be published. -
After each catalog goes live: flip its
distribution/channels.yamlentry tostatus: live, setlinks.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:
| Tier | Meaning | Examples |
|---|---|---|
| 0 | Core registries, published directly by release CI | GitHub Releases, GHCR, Docker Hub, npm |
| 1 | Packaged formats owned by this repo, CI-published | Helm, Homebrew tap, Snap, .deb/.rpm, desktop AppImage |
| 2 | LibreDB-owned copies and listings, bumped by hand | CapRover mirror (deprecated), Railway, Koyeb button, Fly.io config, Render Blueprint |
| 3 | Upstream community catalogs, bumped via PR | CapRover official, Dokploy, Cosmos, Kubero |
| 4 | Partner 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".
| Probe | Channel | What it measures |
|---|---|---|
ghcr-tag-digest | docker-ghcr | that the digest :latest resolves to equals the released version tag's (anonymous pull token — no secret, works from a fork) |
dockerhub-tag-digest | docker-hub-mirror | the 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-channel | snap | the 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-version | winget | the 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 assetspublish-releaserequires (.deb/.rpm, AppImage, the win32 zip) have no switch, because one mistypedfalsethere would silently ship a release with no npm package or no image.parseChannelsrejects 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
channelsjob iscontinue-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 pending — flathub 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.59was built once by manualworkflow_dispatchfrom the post-mergemaincommit (the0.9.59tag carries neitheroperator/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. Flipoperatorhub-communityindistribution/channels.yamlfrompendingtoliveonce the listings are visible, and rememberrelease-config.yamlfor 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 andSNAPCRAFT_STORE_CREDENTIALSis 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
livewith 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.txtis 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
.dmgplus brew cask, the Microsoft Store MSIX and the Tauri updater are still open and need paid signing identities — seedocs/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
win32with 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 viaconfig.authBootstrap=off.