Skip to content

feat(distribution): desktop shell, AppImage release artifact and Flathub manifest (#232) - #235

Merged
cevheri merged 3 commits into
mainfrom
feat/flathub-desktop-232
Jul 28, 2026
Merged

cevheri merged 3 commits into
mainfrom
feat/flathub-desktop-232

Conversation

@cevheri

@cevheri cevheri commented Jul 28, 2026

Copy link
Copy Markdown
Member

Closes the packaging work in #232: a real desktop application, the AppImage release
artifact it produces, and the Flathub manifest that repacks it.

Flathub only accepts sandboxed graphical desktop apps, so the blocker on #232 was the
desktop wrapper itself (the #115 spike's go recommendation). This PR implements the Linux
half of that recommendation and the Flathub packaging on top of it.

What lands

desktop/ - Tauri v2 shell (Rust, 36 unit tests). A window that owns one child
process: the same standalone Next.js server every other channel ships. It picks a free
loopback port, spawns node server.js from the bundled payload with
STORAGE_PROVIDER=sqlite pointed at the per-user data directory, waits for
GET /api/db/health, signs the user in, and shows the workspace. It restarts the sidecar
with backoff if it dies and always takes it down with the window (SIGTERM then SIGKILL,
plus PR_SET_PDEATHSIG so even a killed shell cannot orphan the server). No shell plugin,
so the webview has no command it could invoke.

scripts/build-desktop-appimage.sh stages the payload plus the pinned,
checksum-verified Node runtime, bundles the AppImage, renames it to the repo's asset
convention (libredb-studio-desktop-<version>-linux-<arch>.AppImage + .sha256), and with
--smoke boots the bundled server out of the extracted AppDir and requires a 200 from the
health endpoint - a display-free proof that the bundle is complete.

packaging/flatpak/ holds the manifest template, AppStream metainfo, desktop entry and
launcher for org.libredb.Studio, rendered by scripts/render-flatpak-manifest.mjs (same
pattern as the Homebrew formula renderer, unit tested).
scripts/build-flatpak-local.sh builds, lints and installs it from a locally built
AppImage, so the packaging is verifiable before anything is released.

CI. release-artifacts.yml gains a desktop-appimage job (x64 + arm64, native
runners) and the AppImage becomes a required release asset - the Flathub update checker
resolves that exact name on every release. A new flatpak-smoke.yml builds the AppImage and
repacks it with flatpak-builder@v6 in the privileged Flathub container on both
architectures, path-filtered. It publishes nothing: no flat-manager, no Flathub
credential.

Decisions worth reviewing

  • Flathub repacks the released AppImage instead of building from source. It is our own
    MIT-licensed release artifact, already built and smoke-tested per version, so Flathub
    ships the same bytes as every other channel and there is one build pipeline. This is
    accepted practice (Beekeeper Studio, Signal, Element) but Flathub's written guidance still
    prefers source builds, and flatpak-cargo-generator makes a from-source Tauri build
    realistic - so a reviewer may ask. The submission PR should say this up front;
    packaging/flatpak/README.md records the reasoning. This supersedes the spike doc's
    "offline build with vendored cargo/npm sources" note, which is corrected there.
  • The auth handoff reuses the server's zero-config credentials rather than injecting a
    one-shot password per launch as the spike proposed. The wrapper leaves JWT_SECRET and
    ADMIN_PASSWORD unset, lets the server generate and persist them in auth-bootstrap.json
    (mode 0600) next to the storage database, then has the webview POST them to the existing
    /api/auth/login. No new secret-handling code, no server change, sessions survive
    restarts, and the bootstrap file stays the documented fallback if the handoff ever fails.
    The wrapper clears inherited ADMIN_PASSWORD / AUTH_BOOTSTRAP / storage variables so a
    developer's shell cannot break that contract.
  • org.gnome.Platform//50, not org.freedesktop.Platform. Only the GNOME runtime ships
    webkit2gtk-4.1. The manifest's runtime version, the CI container image tag and the SDK
    in the local instructions must move together.
  • The Flatpak installs three files out of the AppImage - shell binary, Node sidecar,
    payload - and drops the GTK/WebKitGTK stack linuxdeploy bundled. Mixing a bundled
    libwebkit2gtk with the runtime's helper processes crashes the webview.
  • One shared-code change was unavoidable: the session cookie's Secure flag.
    login() marked the cookie Secure whenever NODE_ENV=production, and libsoup - the
    cookie store behind WebKitGTK - discards a Secure cookie delivered over http instead of
    ignoring the flag the way Chromium does on localhost. The handoff therefore logged in
    successfully and then bounced between /login and / forever. Secure is now omitted for
    exactly one case: a request that arrived on a loopback host over plain http, where the flag
    protects nothing because the traffic never leaves the machine. A proxy that terminated TLS
    and forwarded to loopback still gets Secure via x-forwarded-proto; every other
    deployment is unchanged. Eight new tests cover the matrix.
  • desktop/ had to join the payload prune list. Next.js output tracing sweeps the repo
    root, so a local build with a Rust target/ present produced a 1.1 GB release tarball
    before the deny-list caught up (scripts/lib/prune-standalone-payload.sh, issue Trim the standalone payload: Next output tracing pulls repo-root extras #124).
  • Two bundling traps are guarded, not just fixed. Tauri's resource copy drops symlinks,
    and Turbopack resolves the externalized database drivers through hashed symlinks under
    payload/.next/node_modules - so the bundle looked fine, the server logged "Ready", and
    every request died with Failed to load external module ssh2-<hash>. The build script
    materializes those symlinks and the smoke test now asserts the directory exists. Same for
    the Alpine build of sharp's libvips, which makes linuxdeploy fail hard on
    libc.musl-x86_64.so.1. productName also stays space-free so the manifest's build
    commands need no quoting.
  • The AppImage is a hard release gate (like the Windows zip, unlike the snap): it needs
    no credentials, and a release without it would strand the Flathub channel.

Not in this PR

The Flathub submission PR against flathub/flathub and the domain-verification token at
https://libredb.org/.well-known/org.flathub.VerifiedApps.txt are manual steps by design -
the token only exists after submission. The checklist is in packaging/flatpak/README.md
and #232 stays open for it. macOS .dmg, brew cask, the Microsoft Store and the Tauri
updater remain out of scope (all need paid signing identities).

…hub manifest (#232)

Flathub only accepts sandboxed graphical desktop apps, so the blocker on #232 was
the desktop wrapper itself. This adds it, publishes the AppImage it produces, and
repacks that AppImage for Flathub.

desktop/ holds a Tauri v2 shell: a window that owns one child process, the same
standalone Next.js server every other channel ships. It picks a free loopback
port, spawns node server.js from the bundled payload with STORAGE_PROVIDER=sqlite
in the per-user data directory, waits for GET /api/db/health, signs the user in
against the credentials the zero-config first run persisted, and shows the
workspace. It restarts the sidecar with backoff if it dies and always takes it
down with the window (SIGTERM then SIGKILL, plus PR_SET_PDEATHSIG so a killed
shell cannot orphan the server). No shell plugin, so the webview has no command
it could invoke. 38 unit tests cover the pure logic.

scripts/build-desktop-appimage.sh stages the payload and the pinned Node runtime,
bundles the AppImage, renames it to the release-asset convention and, with
--smoke, boots the bundled server out of the extracted AppDir. packaging/flatpak/
holds the manifest template, metainfo, desktop entry and launcher for
org.libredb.Studio, rendered by scripts/render-flatpak-manifest.mjs;
scripts/build-flatpak-local.sh builds, lints and installs it from a locally built
AppImage so the packaging is verifiable before anything is released.

release-artifacts.yml gains a desktop-appimage job (x64 and arm64, native
runners) and the AppImage becomes a required release asset, since the Flathub
update checker resolves that exact name every release. flatpak-smoke.yml builds
the AppImage and repacks it with flatpak-builder@v6 in the privileged Flathub
container on both architectures. Neither publishes to Flathub.

One shared-code change was unavoidable: login() marked the session cookie Secure
whenever NODE_ENV=production, and libsoup - the cookie store behind WebKitGTK -
discards a Secure cookie delivered over http instead of ignoring the flag the way
Chromium does on localhost, so the handoff signed in and then bounced between
/login and / forever. Secure is now omitted only for a request that arrived on a
loopback host over plain http, where it protects nothing; a proxy that terminated
TLS still gets it via x-forwarded-proto.

Verified locally end to end: AppImage smoke, appstreamcli validate,
flatpak-builder-lint on manifest and repo, flatpak run org.libredb.Studio into
the workspace with no password prompt, a PostgreSQL connection over TCP from
inside the sandbox, and a query returning rows in the results grid.
cevheri added 2 commits July 28, 2026 19:07
auth.ts now imports headers() as well as cookies() to decide the session
cookie's Secure flag. mock.module replaces the whole module, so a mock that
returns only cookies makes the import fail at link time with "Export named
'headers' not found" - which is what the per-file coverage run
(tests/run-core.sh) reported for tests/unit/lib/auth-jwt-config.test.ts.

The aggregate `bun run test` did not catch it because mock.module is
process-wide: the auth.test.ts mock, which does export headers, won the shared
process. Verified each file in isolation and through the full coverage gate
(161/161 core files, 100% lines).
…#232)

The AppImage is its own user-facing channel - release CI builds it per arch and
publish-release requires the assets - so it needs a row in channels.yaml and in
the install tables, not just a mention under the Flathub section. status stays
pending until the first release that actually ships it (0.9.59 predates the job).
@cevheri
cevheri merged commit dc3a02d into main Jul 28, 2026
17 of 18 checks passed
@sonarqubecloud

Copy link
Copy Markdown

@cevheri
cevheri deleted the feat/flathub-desktop-232 branch August 20, 2026 20:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant