Geode UI (0.1.1) ships as a packaged desktop application, geode-ui-desktop, built with Electron
and electron-builder
. The desktop build wraps the same React SPA that the headless geode-ui service serves, so there is no separate build of the front end, no second Content-Security-Policy, and no second authentication flow.
Overview
The desktop application is a native window around the existing Geode UI front end. The Electron renderer loads the SPA bundle through a custom geode://app/ privileged protocol rather than over HTTP, which gives the packaged window the same 'self' origin semantics a browser tab would see.
Key characteristics:
- Same React tree as the service. The desktop app reuses the SPA bundle that is embedded in the headless
geode-uibinary; there is no second front-end build to maintain. - Offline-first asset loading. All JS, CSS, fonts, icons, and favicons are served from
geode://app/— no CDN dependency. - Hardened by default. The window runs with context isolation, a sandboxed preload, no Node integration, no
<webview>tags, and deny-by-default permission handlers. - OS integration. Window position and size are persisted, external links open in the OS default browser, and the renderer’s theme tracks the OS light/dark setting in real time.
How it differs from the headless service
Geode UI ships in two packages, and they serve different audiences:
| Package | What it is | Recommended for |
|---|---|---|
geode-ui | Headless, systemd-managed Go service that serves the SPA over HTTP/WebSocket | Servers and shared deployments |
geode-ui-desktop | Electron GUI that wraps the same SPA in a native window | Workstations |
The headless service is documented across the rest of this suite — see Installation and Configuration . The desktop application is the recommended option on individual workstations.
The most important architectural difference is how the front end is loaded:
- The headless service serves the SPA over HTTP at
/from the embeddeddist/bundle. - The desktop app loads the SPA from
geode://app/index.htmlvia Electron’sprotocol.handle, mappinggeode://app/<path>to the bundleddist/<path>. Any unknown path returnsindex.htmlso client-side routes survive a hard reload, and any request that resolves outside the bundle directory returns403.
Installation
On Debian-based systems the desktop app is installed from the Geode UI apt repository:
sudo apt install geode-ui-desktop # electron GUI (recommended on workstations)
The headless service is a separate package:
sudo apt install geode-ui # headless service (systemd-managed)
The release pipeline also publishes desktop installers as GitLab Release assets for each tagged release. The desktop installer formats are:
- macOS:
.dmg - Linux:
.AppImageand.deb - Windows:
.exe
geode-ui-desktop artifacts.For the full set of install paths (apt repo bootstrap, direct dpkg, and co-resident setup alongside the geode server and the headless geode-ui service), see the Installation
page.
Connecting to a Geode UI server
The desktop app is a renderer for the Geode UI front end; the actual graph traffic flows from the SPA to a Geode UI server’s API origin. The renderer in a packaged build must be configured to target the operator-supplied API origin, because the SPA’s 'self' origin under Electron is geode://app — and the WebSocket query channel (/ws/query) goes to the backend server, not back to geode://.
GEODE_SERVER_URL and auto-connect
The packaged build determines its API origin as follows:
- Set the
GEODE_SERVER_URLenvironment variable to point the app at a specific Geode UI server. - When
GEODE_SERVER_URLis not set, the app auto-connects to a Geode UI server onlocalhost:8080.
GEODE_SERVER_URL="https://geode-ui.internal:8080" geode-ui-desktop
# With GEODE_SERVER_URL unset, the app connects to localhost:8080
geode-ui-desktop
connect-src boundary means the API origin is an operator-configuration concern, not something the CSP wording can relax. If the renderer is not pointed at the correct API origin, the /ws/query WebSocket handshake is blocked because 'self' under Electron resolves to geode://app.The Geode UI server you connect to is a normal headless geode-ui instance — start one as described in Configuration
(for example, listening on :8080). Sign in with your Geode credentials exactly as you would in a browser; see Authentication & Security
.
Security model
The desktop window is hardened to keep the renderer — which is the primary attack surface in the desktop threat model — tightly contained.
Window and process baseline
The packaged window enforces the following:
contextIsolation: true,nodeIntegration: false,sandbox: true. The renderer cannot reach Node directly.- No
<webview>tags. setWindowOpenHandlerdenies every opener and routes the URL to the OS default browser viashell.openExternal. An<a target="_blank">cannot spawn another Electron window.will-navigateblocks navigation outside thegeode://origin.
The window.geode bridge
The preload runs with context isolation and the sandbox enabled, and exposes a deliberately small surface to the renderer through contextBridge.exposeInMainWorld:
interface GeodeBridge {
isElectron: true
getVersion(): Promise<string>
openExternal(url: string): Promise<boolean> // https / mailto only
getSystemTheme(): Promise<'light' | 'dark'>
onSystemThemeChange(handler): () => void
}
The renderer detects Electron at runtime via window.geode?.isElectron and falls back to web-native equivalents (window.open, window.matchMedia('(prefers-color-scheme: dark)')) when running in a browser tab. The same React tree therefore runs unchanged in both the desktop app and a browser.
IPC handlers are explicit and validate input. The geode:open-external handler whitelists https: and mailto: URLs only — file:// and custom schemes cannot be launched.
Content-Security-Policy under geode://
The SPA emits its CSP on HTML responses, and it reads:
default-src 'self';
script-src 'self' 'wasm-unsafe-eval';
style-src 'self' 'unsafe-inline';
worker-src 'self' blob:;
connect-src 'self' wss:;
img-src 'self' data:;
font-src 'self' data:;
frame-ancestors 'none';
base-uri 'none';
form-action 'self';
object-src 'none'
Under Electron the SPA loads from geode://app/index.html, and Chromium resolves 'self' to the origin of the document — geode://app — not to a synthesised “Electron” pseudo-origin. As a result the existing CSP works unchanged under geode://:
- All bundle JS, CSS, fonts, images, and favicons load from
geode://app/*and are same-origin, soscript-src 'self',style-src 'self',img-src 'self', andfont-src 'self'all permit them. script-src 'wasm-unsafe-eval'is required by Monaco’s tokenizer and is tied to the Chromium feature, not the origin, so it works under Electron unchanged.worker-src 'self' blob:permits Monaco’s worker, which bootstraps from ablob:URL on first edit.frame-ancestors 'none'is honored even under the custom protocol; the packaged window is the only legitimate frame container.
The one boundary that is not a CSP-wording concern is connect-src 'self' wss: — the WebSocket query channel targets the backend server, so the API origin must be configured (via GEODE_SERVER_URL as described above), otherwise the handshake is blocked.
Permission policy
Electron’s privileged geode:// scheme would otherwise inherit Chromium’s default permission handlers for OS-integration APIs (media, clipboard-write, notifications, geolocation, and others). The desktop app installs deny-by-default handlers when the app is ready: the set of allowed permissions is empty, so every prompt (media, geolocation, notifications, clipboard, and so on) is denied. Both the request and check paths are bound, so navigator.permissions.query({name: '…'}) reports denied without firing a user-facing prompt. The SPA does not request any OS-integration permissions today.
Saved-password handling
The desktop app can remember connection passwords as a UX convenience, comparable to a browser password manager. This is a deliberate trust boundary, not a privileged credential vault.
How saved passwords are stored
The preload exposes saved-password IPC channels on window.geode:
getStoredPassword(profile, username): Promise<string | null>storeSavedPassword(profile, username, password): Promise<boolean>clearStoredPassword(profile, username): Promise<boolean>
The on-disk file, <userData>/secure-store.json, is encrypted via Electron’s safeStorage under the OS keyring entry for the current user and app. Each payload is wrapped in a { v: 2, ciphertext, hmac } envelope, and the HMAC key is a 32-byte random value persisted to a sibling file, <userData>/secure-store.mackey (wrapped via safeStorage, mode 0o600):
<userData>/
secure-store.json # connections + saved passwords (encrypted via safeStorage)
secure-store.mackey # HMAC key (32 random bytes wrapped via safeStorage), mode 0o600
Deleting secure-store.mackey forces the next launch to generate a fresh key. The prior secure-store.json then fails its HMAC and the read returns an empty document — an explicit, recoverable wipe that surfaces as a clean re-login flow rather than a silently broken UI.
The trust boundary
At-rest encryption gates the file against another OS user reading it directly. It does not gate the IPC channels against a renderer running in the same OS user’s process — and the renderer is the attack surface in the desktop threat model.
(profile, username) pairs and read each stored password. Treat a renderer compromise as exposing every stored credential.Defense in depth
The desktop build adds two layers on top of at-rest encryption:
- Unlock-token gate. The main process tracks a per-session unlock token issued at sign-in and expires it after roughly 30 seconds. The IPC handler requires a matching token before returning a password, so a renderer compromise that lacks the freshly issued token receives
nullfrom everygetStoredPasswordcall. - OS-native re-authentication (opt-in). Controlled by the
savedPasswordRequireBiometricpreference, persisted insecure-store.jsonand toggled via thegeode:get-saved-password-biometric-pref/geode:set-saved-password-biometric-prefIPC channels. When the preference is on:- macOS prompts via Touch ID (
systemPreferences.canPromptTouchID()pluspromptTouchID('Unlock saved password')). A rejected prompt returnsnullfromgetStoredPassword. A successful prompt within the last 60 seconds satisfies the gate without re-prompting and also obviates the unlock-token requirement for that read. - Windows is currently a fail-closed stub: turning the preference on returns
nulluntil Windows Hello is wired up via a native module. Opting in on Windows today disables saved passwords until that integration lands. - Linux is currently a fail-closed stub with the same semantics, pending polkit / pkexec integration.
- macOS prompts via Touch ID (
For the broader authentication model — JWT issuance, credentials, and rate limiting — see Authentication & Security .
Window state and external links
- Window state. The app persists window position, size, and maximized state to
<userData>/window-state.json, so it reopens at the same dimensions. - External links. Any URL outside the
geode://origin is forced into the OS default browser viashell.openExternal. The application menu’s “Help → Geode documentation” item openshttps://geodedb.com/docs/. - Theme. The renderer’s theme tracks the OS light/dark setting. The main process pushes a theme-changed event when the OS theme flips, so the UI reacts in real time without polling.
Code signing and packaging notes
Desktop installers are signed in the release pipeline where the corresponding certificates are provisioned. The signing behavior that is relevant to end users:
| Platform | Signing | Notes |
|---|---|---|
Windows .exe | Signed with an EV Authenticode certificate via ssl.com’s eSigner cloud HSM | EV certs are recommended for SmartScreen reputation. |
macOS .dmg | Signed via electron-builder with an Apple Developer ID; hardened runtime is enabled, and builds are notarized via notarytool | Signing and notarization apply when the Apple Developer ID cert is provisioned. |
Linux .AppImage / .deb | Unsigned by default | Provenance comes from the signed GitLab release tag; GPG signing can be layered on at the apt-repository level. |
App icons are produced from a single 512×512 PNG, from which electron-builder builds the .icns and .ico variants automatically.
Related pages
- Installation
— apt repo bootstrap, direct
dpkg, and co-resident setup - Configuration
— running a headless
geode-uiserver for the desktop app to connect to - Authentication & Security — sign-in, JWT, and rate limiting
- Query Editor & Visualization — the editor and graph surfaces the desktop app renders