LibreDB Studio desktop shell
July 30, 2026 ยท View on GitHub
A Tauri v2 window around the standalone LibreDB Studio server. The shell adds no
features: it starts the same node server.js payload every other channel ships,
waits for it to become healthy, logs the local user in and shows the app. Issue
#232; the design it
implements is docs/DESKTOP_WRAPPER_SPIKE.md
(issue #115).
The desktop build is what makes the AppImage and the Flathub listing possible - Flathub rejects headless server applications.
Layout
desktop/
src/index.html splash page shown while the server boots
src-tauri/
tauri.conf.json bundle config; app version comes from the repo-root package.json
desktop-entry.hbs desktop-file template (display name, StartupWMClass)
src/lib.rs boot sequence and supervision (the Tauri wiring)
src/net.rs free-port pick + /api/db/health gate
src/sidecar.rs the child process: env contract, log tail, shutdown
src/handoff.rs reads the bootstrap password, builds the login script
src/layout.rs finds the bundled node runtime and payload
src/backoff.rs restart policy
icons/ bundle icons (generated from public/logo.svg)
bin/node-<triple> staged by the build script, git-ignored
payload/ staged by the build script, git-ignored
How it boots
- The window opens on the bundled splash page immediately.
- A background thread picks a free loopback port and spawns
node server.jsfrom the bundled payload withSTORAGE_PROVIDER=sqliteandSTORAGE_SQLITE_PATHinside the per-user data directory ($XDG_DATA_HOME/org.libredb.Studioon Linux,~/.var/app/org.libredb.Studio/data/...inside Flatpak). - It polls
GET /api/db/healthuntil it answers 200 (30 s budget). - It reads the admin password the server's zero-config first run (#109) wrote to
auth-bootstrap.jsonin that data directory and navigates the webview to/. With a session already in the webview's cookie jar that is the workspace; on a first run the server redirects to/login, where the shell evaluates a small script that POSTs the credentials to the existing/api/auth/loginroute and replaces the page with/. Landing on/either way is deliberate - going to/logindirectly would send a returning admin to the admin dashboard instead. - If the server dies later, the shell restarts it (1 s, 5 s, 15 s) and then gives up with the captured server log shown on the splash page.
Notes on the choices, since they are easy to get wrong later:
- No credentials are invented by the shell.
JWT_SECRETandADMIN_PASSWORDare left unset so the server generates and persists them itself, mode 0600, next to the storage database. Sessions therefore survive restarts, andauth-bootstrap.jsonstays the documented fallback if the automatic handoff ever fails - the user can read the password out of it and log in by hand. The shell also clears inheritedADMIN_PASSWORD/AUTH_BOOTSTRAP/ storage variables so a developer's shell cannot break that contract. - The server is never exposed.
HOSTNAME=127.0.0.1and a random port; the parent process'HOSTNAME(the machine name on most desktops) would otherwise become the bind address. - The sidecar cannot outlive the shell. Window close and app exit both run a
SIGTERM-then-SIGKILL shutdown,
Droprepeats it, and on Linux the child getsPR_SET_PDEATHSIGso even a killed shell takes the server with it. - No shell plugin, no IPC surface. The child process is spawned from Rust
with
std::process, so the webview has no command it could invoke. productNamestays space-free. It names the bundled resource directory (usr/lib/<productName>/payload), which the Flatpak manifest's build commands and the build scripts then handle without quoting gymnastics. The display name lives indesktop-entry.hbsand the windowtitleinstead; the AppImage smoke test fails the build if a space reappears.- Symlinks do not survive bundling. Turbopack resolves the externalized
database drivers (
pg,mysql2,mongodb,ssh2,better-sqlite3) through hashed symlinks underpayload/.next/node_modules, and Tauri's resource copy drops symlinks silently - the directory is simply missing from the bundle and every request fails withFailed to load external module <driver>-<hash>, while the server still logs "Ready". The build script materializes those symlinks as real directories and the smoke test asserts the directory is present.
Development
# One-time system dependencies (Debian/Ubuntu names)
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev librsvg2-dev patchelf file
# Stage the payload + node sidecar, then bundle an AppImage (keeps the staging
# directories so `tauri dev` works straight after)
scripts/build-desktop-appimage.sh dist --smoke --keep-stage
# Iterate on the shell itself against the staged payload
cd desktop/src-tauri && bunx @tauri-apps/cli@2.11.4 dev
# Unit tests (pure logic: port pick, env contract, handoff, backoff, shutdown)
cd desktop/src-tauri && cargo test
cargo test needs Rust >= 1.88 (the floor of the Tauri 2.11 dependency tree).
Packaging
- AppImage and GUI .deb: one
scripts/build-desktop-appimage.shrun produces bothlibredb-studio-desktop-<version>-linux-<arch>.AppImageandlibredb-studio-desktop_<version>_<debarch>.deb, each with a.sha256sidecar.--smokechecks the .deb's layout and boots the bundled server out of the extracted AppDir (no display needed) to prove the bundle is complete. Release CI runs it per architecture in.github/workflows/release-artifacts.yml; both artifacts are required release assets.--deb-onlyskips the AppImage, which is the way to build on a host without the linuxdeploy GTK toolchain (librsvg2-devand friends). - The bundled Node sidecar is named
libredb-studio-node, notnode. The .deb installs it into the real/usr/bin, wherenodeis owned by the distronodejspackage and dpkg would refuse the install.externalBinintauri.conf.json,NODE_BINin the build script andNODE_BINinsrc/layout.rsall have to agree;layout.rsstill probes the old name so an AppDir built before the rename keeps working. - Flatpak / Flathub:
packaging/flatpak/repacks that AppImage. See its README for the local build and the submission steps. - FlatPark:
packaging/flatpark/pins the GUI .deb as Flatpak extra-data instead, because FlatPark does not accept AppImages.