Desktop App Architecture
Status: Phase 1 Linux MVP fully wired. The Tauri v2 shell ships, 88
adapter families are linked into the router (Waves 0-3 complete),
TauriEnv::fetch_rendered is implemented via a hidden Tauri
WebviewWindow (same WebKitGTK engine as the main UI — no SaaS
dependency), the updater is armed with a real Ed25519 keypair, and
all three Linux bundles (deb/rpm/AppImage) produce cleanly from the
tag-driven release workflow. Remaining open work is Phase 2 multi-OS
(macOS + Windows) and AUR PKGBUILD activation.
This doc supersedes the desktop sections of
jobcache/wiki/content/architecture/desktop-edge-runtime.md
on points where they disagree. The Jobcache doc was written before the
workspace-bound + network-loaded model crystallised.
STATUS — AS OF 2026-05-30
SHIPPED
- Tauri v2 shell at
ui/desktop/—@cv/desktoppackage with tauri-cli ^2 wrapper scripts,src-tauri/depending on tauri 2 + http/updater/single-instance/os/fs plugins + jobcache + every adapter crate.tauri.conf.jsonpoints the main window athttps://careervector.corbet.ch/with CSP allowlist, ships deb/rpm/appimage bundle targets, and embeds the real Ed25519 updater pubkey.local-shell/index.htmlsplash fallback is in place.cargo tauri infoall-green;cargo check -p careervector-desktopclean. - 88 adapter families wired into
runtime::router::Router::default(ui/desktop/src-tauri/src/runtime/router.rs):- Wave 0 (19, ATS APIs / static HTML): ashby, bamboohr, breezyhr, eightfold, greenhouse, homerun, icims, jibe, lever, oracle-hcm, prospective, recruitee, remoteok, rss-feed, sf-rmk, smartrecruiters, teamtailor, workable-widget, workday.
- Wave 1 (8, additional static HTML): custom-html, joincom,
personio, persoware, refline, sf-html, softgarden, umantis.
(
xiag-boardwas superseded byyoustyand moved tocrates-archive/.) - Wave 2 (5, JS-rendered): csod, hibob, phenom, playwright-html,
sf-spa. These call
env.fetch_renderedand currently propagateFetchError::Unavailable("rendered fetch")until a headless backend is wired. - Wave 3 (56, employer-bespoke + public boards + Yousty platform):
- Public boards (11): linkedin, xing, indeed, glassdoor, adzuna,
reed, totaljobs, cwjobs, weworkremotely, wttj, hackernews.
Hostile boards (linkedin, xing, indeed, glassdoor) use
env.fetch_rendered; the rest plain HTTP. - Swiss job boards (13): jobs-ch, jobup-ch, jobwatch, jobscout24, ictjobs-ch, ictcareer, itjobs-ch, swissdevjobs, jobwinner, publicjobs, ipersonal, stelle-admin, eures.
- Recruitment agencies (15): hays, randstad, michaelpage, adecco, manpower, experis, roberthalf, swisslinx, coopersgroup, huxley, rmgroup, morganphilips, darwinrecruitment, nexus, divisions.
- Single-employer JSON APIs (10): glencore, un, alpha, bcge, bankcler, gavi, citi, creditagricole, cargill, cbre.
- Specialized vertical boards (6): reliefweb, hotelcareer, hoteljob, europharmajobs, devjobsscanner, wearedevelopers.
- custom-html catch-all extension (9 new tenants): sbb, axpo,
alpiq, bkw, migros, securitas, cern, syzgroup, startupch —
appended to the existing
custom-htmlSOURCES table (no new crate; the catch-all family now carries 16 tenants total).
- Public boards (11): linkedin, xing, indeed, glassdoor, adzuna,
reed, totaljobs, cwjobs, weworkremotely, wttj, hackernews.
Hostile boards (linkedin, xing, indeed, glassdoor) use
jobcache::Env::fetch_renderedcapability extension landed incrates/jobcache/src/adapter.rs.TauriEnvinui/desktop/src-tauri/src/runtime/env.rsnow implements it via a hidden Tauri WebviewWindow — same WebKitGTK engine the main UI already uses, zero extra binary. Bridge: initialization script invokes thecv_renderer_htmlTauri command with the post-render HTML after a 2s settle delay;RendererRegistry(anArc<Mutex< HashMap<label, oneshot::Sender>>>) fans completions back to the spawning task. Race-safe: labels generated by atomic counter (cv-render-<8-hex>), per-call timeout + cleanup on success / timeout / JS error. Chromium-via-chromiumoxidestays intools/render-bench/as the documented fallback if WebKit ever proves insufficient for a future adapter family.- Updater armed: Ed25519 keypair generated 2026-05-30 (no password).
Pubkey embedded in
ui/desktop/src-tauri/tauri.conf.json; private key sops-encrypted at~/.agent/secrets/desktop-updater.yml. CI consumesTAURI_SIGNING_PRIVATE_KEY+TAURI_SIGNING_PRIVATE_KEY_PASSWORDfrom the CircleCI context/project env used bydesktop_release_linux. - AppImage post-bundle Wayland fix —
ui/desktop/scripts/post-bundle-appimage.shreplaces the autogenerated AppRun withui/desktop/scripts/AppRun.patched(runtime backend detection) and re-packs + re-signs the AppImage. Wired intoui/desktop/package.json'sbuildscript and the CircleCIdesktop_release_linuxjob. - All three Linux bundles producing clean artifacts: .deb ~6.8 MiB, .rpm ~6.8 MiB, AppImage ~108 MiB.
rust-toolchain.tomlat repo root pins stable + rustfmt/clippy/ rust-analyzer/rust-src/llvm-tools +wasm32-wasip1andwasm32-unknown-unknowntargets.SubHeaderBar.leadingSlot+DownloadDesktopButton+ScrapingPillRuntimeCapabilitiesTS API +@tauri-apps/apidep landed.
- CircleCI
desktop_release_linux— Linux-only, tag-driven ondesktop-v*, running onresource_class: corbet/unraid.
IN PROGRESS / NEXT
TauriEnv::fetch_renderedbackend. Default impl returnsFetchError::Unavailable("rendered fetch")so the five Wave-2 adapters (csod, hibob, phenom, playwright-html, sf-spa) cleanly signal "needs rendered fetch backend" instead of failing as transport errors. Backend choice between Browserbase (~/.agent/secrets/browserbase.yml) and Firecrawl (~/.agent/secrets/firecrawl.yml) is pending — pricing tier and monthly call budget drive it more than technical fit. See the comment block at the end ofui/desktop/src-tauri/src/runtime/env.rs.Wave 3 — LANDED 2026-05-30. 55 new adapter crates wiring up public boards (LinkedIn, Xing, Indeed, Glassdoor, Adzuna, Reed, TotalJobs, CWJobs, WeWorkRemotely, WTTJ, HackerNews), Swiss boards (jobs-ch, jobup-ch, jobwatch, jobscout24, ictjobs-ch, ictcareer, itjobs-ch, swissdevjobs, jobwinner, publicjobs, ipersonal, stelle-admin, eures), recruitment agencies (hays, randstad, michaelpage, adecco, manpower, experis, roberthalf, swisslinx, coopersgroup, huxley, rmgroup, morganphilips, darwinrecruitment, nexus, divisions), single-employer JSON APIs (glencore, un, alpha, bcge, bankcler, gavi, citi, creditagricole, cargill, cbre), and specialized vertical boards (reliefweb, hotelcareer, hoteljob, europharmajobs, devjobsscanner, wearedevelopers). Plus 9 new Swiss employer tenants appended to the existing
custom-htmlcrate (sbb, axpo, alpiq, bkw, migros, securitas, cern, syzgroup, startupch). Was the original "~37 employer-bespoke crates" plan; expanded to 55 once the public boards and Swiss aggregators were folded in.Wave 3 follow-up. Per-employer adapters that don't fit any beyond the multi-tenant families.
Public job boards. LinkedIn, Xing, Indeed, Adzuna, Reed, Stellenanzeiger, Jobwatch — each as its own Rust crate, dropped into
Router::defaultvia oneextend_withline.TS→Rust ports. jobs-ch, jobup-ch, yousty currently exist as TypeScript adapter implementations and need Rust ports for the desktop dock.
AUR PKGBUILD wired into the release workflow (placeholder job exists; needs AUR SSH key + maintainer credentials to be activated).
DEFERRED
- Phase 2 — macOS + Windows matrix. Needs Windows self-hosted
runner (Julian's Windows VM) + macOS code-signing certs. The
tauri-apps/tauri-actionswap is straightforward once both are in place. - Broker lease / observation submission loop in the Tauri runtime — blocked until the JobCache broker endpoints are wired.
- System tray, native notifications, deep links — phase 3.
- Full contribution dashboard surface — phase 3.
- Multi-workspace switcher — UX TBD.
PHASED PLAN
- Phase 1 (landed). Tauri v2 shell, remote-loaded UI pointing at
careervector.corbet.ch, 89 adapter families wired intoRouter::default(Wave 0 + Wave 1 + Wave 2 + Wave 3), Download + Scraping surface inSubHeaderBar, Linux-only tag-driven CircleCI release job producing AppImage/deb/rpm/tarball, armed Ed25519 updater, AppImage Wayland-fix post-bundle step. AUR PKGBUILD job stubbed. - Phase 2.
TauriEnv::fetch_renderedbackend (Browserbase vs Firecrawl decision pending). Wave 3 employer-bespoke crates + public-board adapters + jobs-ch/jobup-ch/yousty TS→Rust ports. Multi-OS CI matrix (windows-self-hostedrunner on Julian's VM + a macOS runner). Mac/Windows code signing. AUR publish job activated. Broker lease/observation loop on top of the Tauri runtime once the JobCache broker endpoints are ready. - Phase 3. System tray, native notifications, deep links, auto-start pref. Full contribution dashboard surface inside the workspace UI. Multi-workspace switcher (UX TBD — see Open Decisions).
Purpose
The desktop app is a Tauri-shelled CareerVector for users who want one or more of:
- Better scraping — native HTTP without browser CORS restrictions, delegated to Rust adapters that the cloud Jina chain can't match on quality.
- Contribution — opt-in spare-compute capacity for JobCache top-down ingestion while the app is open (per the desktop-edge-runtime contribution model).
- OS integration — proper window, system tray (later), native notifications (later), single-instance enforcement, file associations.
It is not an offline-first editor, not a local data store, not a desktop-only CareerVector variant. Workspaces stay collaborative; one user installing the desktop app does not break sharing with the other nine people on that workspace.
The big idea: thin shell + network-loaded UI
Tauri's default pattern bundles the entire SvelteKit build into the binary as static assets. That makes every UI change a desktop release. Going-nuts territory.
Tauri v2 supports pointing the webview at a remote URL instead. We use that:
| Component | Where it lives | Update channel |
|---|---|---|
Rust shell (Tauri runtime, native commands, adapter crates linked as rlib) |
binary-resident | Tauri updater, rare releases |
| Workspace-bound bootstrap manifest | binary-resident at first launch, persisted to OS app-data | per-tag release |
| Svelte UI | NOT bundled — webview loads https://{workspace-host}/ directly |
Cloudflare cache invalidation on every web deploy |
Consequences:
- UI changes ship to desktop users automatically on next app launch. Zero desktop release required.
- Desktop release tags (
desktop-vX.Y.Z) fire only when Rust shell, adapter crates, or Tauri config change. - Steady-state cadence drops from "release per commit" to maybe one desktop release per quarter.
- Reinforces "no persistent offline" — without network the app can't load the UI, but that wasn't the use case anyway.
Security: loading a remote app gives the loaded code access to Tauri IPC
commands, so we explicitly allowlist the production domains in
tauri.conf.json (app.security.csp + withRemoteDomainIpcAccess per
Tauri v2 docs on the remote-app pattern).
Only careervector.corbet.ch and jobcache.corbet.ch (and equivalents
for any future workspace hosts) can invoke invoke(). Random pages the
user navigates to inside the webview get no IPC.
Components
ui/ existing SvelteKit app (unchanged for desktop)
├── src/ Svelte source — one codebase, web + desktop both consume it
│ └── lib/components/workspace/ SubHeaderBar mount glue (NEW)
│ ├── WorkspaceIdentityEditor.svelte
│ ├── WorkspaceIdentityReadOnly.svelte
│ ├── WorkspaceActionButtons.svelte
│ └── workspace-actions.ts
├── build/ output from `bun --filter @cv/ui build` — served by Cloudflare
└── desktop/ Tauri shell only, no duplicate UI
├── package.json @cv/desktop — npm scripts wrapping tauri-cli ^2
├── README.md build + run notes
├── local-shell/
│ └── index.html splash fallback if remote UI is unreachable on launch
├── scripts/
│ ├── post-bundle-appimage.sh re-pack AppImage with Wayland-capable AppRun
│ └── AppRun.patched runtime backend detection (X11 if available, else Wayland)
└── src-tauri/
├── Cargo.toml tauri 2 + plugins (http, updater, single-instance, os, fs)
│ + jobcache + all 33 jobcache-adapter-* crates
├── tauri.conf.json remote URL pointer (https://careervector.corbet.ch/),
│ CSP allowlist, deb/rpm/appimage bundle targets,
│ armed Ed25519 updater pubkey
├── build.rs
├── gen/ Tauri-generated artifacts (gitignored)
├── icons/ placeholder RGBA PNGs + icon.ico + icon.icns
├── capabilities/
│ └── default.json Tauri v2 permission set for the main window
└── src/
├── main.rs Tauri entry; configures windows, loads workspace bootstrap
├── lib.rs crate root, re-exports
├── commands.rs #[tauri::command] surface invoked from Svelte
├── state.rs tiny JSON store: bindings, device id, prefs
├── enrollment.rs first-launch bootstrap consumption
└── runtime/
├── mod.rs runtime module root
├── env.rs TauriEnv: Env impl over Tauri http plugin (reqwest).
│ fetch_rendered intentionally NOT overridden — propagates
│ FetchError::Unavailable until a backend is wired.
└── router.rs URL → adapter routing for scrapeUrl. Collects
all_adapters() from every linked family into one
Vec<Box<dyn Adapter>>; first-prefix-match.
(lease.rs + leader.rs deferred — phase 2 with broker)
rust-toolchain.toml at the repo root pins the toolchain (stable +
rustfmt/clippy/rust-analyzer/rust-src/llvm-tools + wasm32-wasip1 +
wasm32-unknown-unknown), so anyone with rustup gets the right
environment automatically. The careervector-desktop crate is a
member of the root Cargo workspace.
The Svelte side adds a RuntimeCapabilities interface (already mentioned
in desktop-edge-runtime.md):
interface RuntimeCapabilities {
canNativeFetch: boolean; // true under Tauri, false in browser
canMatchUrlToAdapter(url: string): Promise<{ adapterId: string; method: string } | null>;
observeUrl(url: string): Promise<Observation | null>; // local Rust adapter path
// ... lease/observation APIs land in phase 2
}
Web build reports canNativeFetch: false. Tauri build reports true
and exposes the IPC methods. Callsites check the capability and dispatch
accordingly.
State model: multi-install peer devices
Each desktop install is its own enrolled device (device_id). Multiple
installs (Julian on laptop + Julian on desktop + Julian's collaborator on
their laptop) are peer devices, not competing copies. The CRDT/Yjs
realtime sync layer already handles N writers per workspace, so the
desktop just becomes one more participant.
Local state on disk is intentionally minimal:
~/.config/CareerVector/state.json Linux (XDG_CONFIG_HOME/CareerVector/)
~/Library/Application Support/CareerVector/state.json macOS
%APPDATA%\CareerVector\state.json Windows
Contents:
{
"bindings": [
{
"workspace_id": "...",
"workspace_url": "https://...",
"broker_url": "https://...",
"device_id": "...",
"enrolled_at": "..."
}
],
"active_binding": "<workspace_id>",
"device_runtime_version": "0.1.0",
"contribution_enabled": true,
"prefs": { "theme": "auto", "auto_start": false }
}
Notably not present:
- workspace data (jobs, CVs, scoring, etc. — all in cloud Yjs)
- credentials beyond the workspace's anonymous slug (Link = Identity per CareerVector §4)
- cached scrapes (each scrape is one Observation, submitted live)
- Yjs persistence
If state.json is deleted, the user re-enrolls and rejoins workspaces
with no data loss — workspace state is in the cloud.
scrapeUrl: native first, cloud fallback
When a user pastes a job URL into a workspace from within the desktop app,
the existing scrapeUrl() call site checks the runtime:
- Tauri runtime? → call
invoke("observe_url", { url }). 1a. Rust router matches URL against every loaded adapter'sdescriptor().homepage(first-prefix-match, with scheme/www.variants normalised). If an adapter matches, the router callsadapter.observe(url, env, opts)against the nativeTauriEnv(no CORS). 1b. No adapter matched? → fall back to the cloud Jina chain. - Browser runtime? → existing cloud Jina chain path.
Marketing line: "Desktop scrapes better than the web. That's the deal."
Adapter dispatch (current)
runtime::router::Router::default() (in
ui/desktop/src-tauri/src/runtime/router.rs) collects
all_adapters() from every linked family into one flat
Vec<Box<dyn Adapter>>. Each multi-tenant family (Greenhouse, Workday,
Lever, etc.) returns its full tenant set; static-HTML families that
serve one tenant per crate return a singleton vec.
Router::match_url iterates in order and returns the first adapter
whose descriptor().homepage is a prefix of the input URL (normalising
http/https and www. variants). Per-family homepage prefixes don't
overlap, so iteration order is irrelevant. Router::observe_url
returns Ok(None) when no adapter matches — the UI surfaces that as
"no native adapter — fall back to cloud Jina".
JS-rendered families (csod, hibob, phenom, playwright-html,
sf-spa) call env.fetch_rendered(req) inside their observe(). The
default TauriEnv impl in
ui/desktop/src-tauri/src/runtime/env.rs does not override
fetch_rendered, so the trait default returns
FetchError::Unavailable("rendered fetch"). These adapters surface
that back to the router as a clean "needs rendered backend" signal —
not a transport failure — and the runtime can mark the source as
skipped until a backend is wired.
Adding a new adapter family
Drop the crate under crates/, add a path = "../../../crates/jobcache-adapter-NAME" dependency to
ui/desktop/src-tauri/Cargo.toml, and append one
extend_with(&mut adapters, jobcache_adapter_NAME::all_adapters())
line to Router::default. No router restructuring needed.
Workspace enrollment
Per the chosen flow (workspace-bound bootstrap bundle):
- User clicks Download desktop app inside a CareerVector workspace.
- Server creates a short-lived enrollment record:
{ "enrollment_id": "...", "workspace_id": "...", "workspace_url": "https://careervector.corbet.ch/w/<slug>", "broker_url": "https://jobcache.corbet.ch/api", "expires_at": "2026-05-30T12:00:00Z" } - Browser downloads a generic, signed Tauri binary plus a per-workspace
workspace-bootstrap.jsonfile containing the enrollment record above. - On first launch, the Tauri shell looks for
workspace-bootstrap.jsonnext to the binary (or in the OS download dir), consumes the enrollment token viaPOST {broker_url}/devices/enroll, gets back a durabledevice_id, persists it tostate.json, deletes the bootstrap file. - Subsequent launches read
state.jsonand go straight to the bound workspace URL inside the webview.
If no bootstrap file is found at first launch (e.g., user downloaded generic binary from elsewhere), the shell shows an enrollment screen where they paste a workspace URL manually as a fallback.
Distribution: Linux package matrix
MVP (Phase 1) — Tauri-native bundle outputs from one CI build. Current artifact sizes for the x86_64 build at version 0.0.1:
| Format | Audience | Size | Notes |
|---|---|---|---|
| AppImage | universal, all distros | ~108 MiB | single file, no install needed; self-contained (bundles webkit2gtk + GTK). Patched AppRun handles Wayland-only sessions (see below). |
| .deb | Debian, Ubuntu, Mint, Pop!_OS, Elementary, MX, Sparky, antiX | ~6.8 MiB | apt installs; depends on system libwebkit2gtk-4.1-0 + libgtk-3-0 |
| .rpm | Fedora, RHEL, CentOS Stream, openSUSE, Mageia, Rocky, Alma | ~6.8 MiB | dnf/zypper installs |
| tarball (.tar.gz) | distro-agnostic, anyone happy to extract + run | small | raw binary, packaged in the CI workflow alongside the official bundles |
AUR (careervector-bin) |
Arch, CachyOS, EndeavourOS, Manjaro, Garuda | n/a | PKGBUILD fetches the GitHub release; the publish job is stubbed in CI, waiting on AUR maintainer credentials |
Phase 2 — store/sandbox distribution (each is a half-day setup):
| Format | Audience | Notes |
|---|---|---|
| Flatpak (Flathub) | universal, growing share | sandboxed; visible in GNOME Software, KDE Discover, Pop!_Shop, AppCenter |
| Snap (Snap Store) | Ubuntu defaults | sandboxed; visible in Ubuntu Store |
| Nix flake (nixpkgs PR) | NixOS, Home Manager users | nix run, home.packages = [pkgs.careervector] |
| Alpine APK (aports template) | Alpine, postmarketOS | apk add |
Phase 3 — community-contributed / by request:
| Format | Audience | Notes |
|---|---|---|
| Gentoo ebuild (overlay) | Gentoo, Funtoo | source-built, USE flags |
| Void Linux xbps template | Void | accepts PR upstream once it stabilises |
| Solus eopkg | Solus | recipe in their repo |
| Pacstall | Ubuntu power-users | PPA-like alt |
| Chaotic-AUR | Arch users avoiding AUR build | mirrors the AUR binary |
| Slackware SlackBuild | Slackware | small audience |
Out of scope (not Linux): NetBSD pkgsrc, FreeBSD ports.
AppImage Wayland fix
The AppImage produced by Tauri's bundler ships with an autogenerated
AppRun script from linuxdeploy-plugin-gtk that hard-codes
GDK_BACKEND=x11. That hardcode is a workaround for
tauri-apps/tauri#8541,
a Tauri 1.x bug. Tauri 2.x handles Wayland natively, so the workaround
is no longer needed — and actively harmful on Wayland-only compositors
that don't auto-export DISPLAY (niri and similar minimal wlroots
compositors). On those sessions GTK init hard-fails before the webview
ever loads.
Fix: ui/desktop/scripts/post-bundle-appimage.sh runs after
cargo tauri build and:
- Replaces
target/release/bundle/appimage/CareerVector.AppDir/AppRunwithui/desktop/scripts/AppRun.patched. The patched script sources linuxdeploy-plugin-gtk's hook for theme/path env, unsets the forcedGDK_BACKEND, then picks a backend at runtime: prefer X11 if aDISPLAYsocket is discoverable (including Xwayland sockets the compositor opened but didn't auto-export), otherwise fall back to native Wayland. - Re-packs the
.AppImagein place viaappimagetool(extracted from Tauri's cachedlinuxdeploy-plugin-appimage.AppImage). - If
TAURI_SIGNING_PRIVATE_KEYis set in env (CI path), invalidates the original.sigand re-signs the new bundle so the updater accepts it.
The script is wired into both the local build path
(ui/desktop/package.json's build script chains
cargo tauri build && bash scripts/post-bundle-appimage.sh) and CI
(the "Patch AppImage AppRun for Wayland-only sessions" step in the CircleCI
desktop_release_linux job).
CI / release cadence
- Trigger: desktop release builds run only for tags matching
desktop-v*, plus explicit CircleCI dispatch for manual dry-runs. Commits tomaindo not trigger desktop CI. - Job: CircleCI
desktop_release_linuxonresource_class: corbet/unraid. - MVP (Phase 1): single Linux runner on
corbet/unraid. Produces AppImage + .deb + .rpm + raw-binary tarball. AUR PKGBUILD publish job is stubbed (see RELEASE PROCESS for the activation prerequisites). - Phase 2: matrix expands to
windows-self-hosted(Julian's Windows VM as registered runner) and a macOS runner. - Updater signing is armed at MVP. Bundle signatures use the real
Ed25519 keypair; CI consumes both
TAURI_SIGNING_*secrets. OS code signing (Mac/Windows) is still phase 2 — when those land, document the "trust this app first launch" dance for any unsigned interim builds.
The job uses cargo tauri build directly (rather than
tauri-apps/tauri-action) so the bundling step composes cleanly with
the Wayland-fix post-bundle step. The release upload step attaches the
four-glob list (AppImage, deb, rpm, tarball) to the GitHub Release. AUR publish
runs as a follow-up publish-aur job after the Linux artifacts land — currently a
placeholder waiting on AUR maintainer credentials.
RELEASE PROCESS
Phase 1 — Linux-only, tag-driven. The release lane is the CircleCI
desktop_release_linux job on resource_class: corbet/unraid.
Cut a release by pushing a tag matching desktop-vX.Y.Z:
git tag desktop-v0.1.0
git push origin desktop-v0.1.0
The job then:
- Checks out the repo on the
corbet/unraidLinux runner. - Installs the Linux system deps Tauri needs
(
libwebkit2gtk-4.1-dev,libgtk-3-dev,libayatana-appindicator3-dev,librsvg2-dev,patchelf,libfuse2) plus the latestlinuxdeploycontinuous build. - Installs the pinned Rust toolchain via
rust-toolchain.toml. - Runs
cargo tauri build --bundles deb,rpm,appimagewithNO_STRIP=trueand bothTAURI_SIGNING_*secrets in env. Bundles land undertarget/release/bundle/(workspace root, not underui/desktop/src-tauri/target/— the desktop crate is a workspace member). - Runs
bash ui/desktop/scripts/post-bundle-appimage.shto patch the AppImage's AppRun for Wayland-only sessions and re-sign the re-packed bundle. - Packages the raw
careervector-desktopbinary ascareervector-${tag}-x86_64-linux.tar.gz. - Publishes the GitHub Release, attaching the
.AppImage/.deb/.rpm/.tar.gzartifacts. Tags containing-(e.g.desktop-v0.1.0-beta) flag the release as a pre-release.
The AUR publish job (PKGBUILD push to AUR via SSH) is stubbed but not
yet wired — careervector-bin will fetch the GitHub release artifacts
once the AUR SSH key + maintainer credentials are in place. The
implementation outline lives in the desktop_release_linux job notes.
Phase 2 expands the matrix to windows-self-hosted (Julian's Windows
VM as a registered runner) and a macOS runner. Both
need real code-signing certs at that point — Windows EV cert and Apple
Developer ID — to avoid first-launch friction.
Updater
Armed. Tauri's built-in updater plugin is configured with a real Ed25519 keypair.
Keypair: generated 2026-05-30 with
cargo tauri signer generate(no password).Public key: embedded in
ui/desktop/src-tauri/tauri.conf.jsonunderplugins.updater.pubkey. Compiled into the binary.Private key: sops-encrypted at
~/.agent/secrets/desktop-updater.yml. Never committed; never read in plain into chat or logs.CI signing: CircleCI
desktop_release_linuxreadsTAURI_SIGNING_PRIVATE_KEYandTAURI_SIGNING_PRIVATE_KEY_PASSWORDfrom the CircleCI context/project env on both thecargo tauri buildstep and the post-bundle re-sign step. The one-time CircleCI secret setup commands are documented inui/desktop/README.mdunder "Updater".Endpoint (placeholder URL — Worker route not yet hosted; updater will silently 404 until manifest hosting is set up — that's the next deploy concern):
https://careervector.corbet.ch/desktop/{{target}}-{{arch}}/{{current_version}}.jsonTauri substitutes
{{target}}(e.g.linux),{{arch}}(e.g.x86_64), and{{current_version}}at poll time.Manifest shape, signature scope: the per-bundle
.sigis what Tauri verifies — the manifest JSON envelope itself is unsigned. An unsigned Windows install (Phase 2) can still verify it's pulling a real update from us via the embedded pubkey.
Open decisions
Resolved
Where does the "Download desktop app" button live in the workspace UI?Resolved —DownloadDesktopButtonlives inSubHeaderBar.leadingSlot, theScrapingPilllives inSubHeaderBar.actionsSlot. Both are wired throughlib/ui-product'sSubHeaderBarand mounted byui/src/routes/[wsId]/+layout.svelteandview/[wsId]/+layout.svelte.What desktop-specific functionality goes in the desktop UI?Mechanism resolved — Svelte detects Tauri via theRuntimeCapabilitiesTS API (canNativeFetch, etc.). The inventory of desktop-only surfaces remains open (see #4 below).
Still open (UX session)
- System tray, native notifications, deep links — assigned to phase 3.
- Auto-start on boot — exposed as a pref under
Settings → Desktop? Default off perdesktop-edge-runtime.mdproduct guardrails. Also: what other desktop-only panels/actions should theRuntimeCapabilitiesfeature flag gate? Inventory TBD. - Contribution dashboard surface — where in the workspace UI does a user see "you've contributed N scrapes / N minutes / N tasks this month"? Builds trust + visibility for the contribution model. Assigned to phase 3.
- Multi-workspace switcher — how does the user switch between workspaces inside the desktop app? Browser-style URL bar? Sidebar workspace list? Window-per-workspace?
These belong to the UX session that follows this doc.