Git Chat Desktop (macOS) — Packaging + Distribution Design Doc
Status: Draft (Milestone/Story reference)
Problem
We want to share Git Chat with friends as a macOS app (e.g., .app/.dmg) that:
- launches reliably on any Mac without installing Node/npm or cloning the repo
- runs terminal commands locally (IDE-like, VS Code-adjacent)
- can eventually auto-update so testers get new features quickly
Goals
- Out-of-the-box: no system Node dependency, no “npm install” for end users
- Local-first: backend runs on the user’s machine and executes commands locally
- Safe-by-default: no remote network command execution exposure
- Fast iteration: simple dev workflow to build/share new DMGs
- Future: support auto-update (later: code signing + notarization)
Non-goals (for this milestone)
- Mac App Store distribution
- Windows/Linux packaging
- Hard multi-user security model (this is a local developer tool)
Current State (As-Is)
Frontend (web-frontend/)
- React + TypeScript + Vite; uses Socket.IO client.
- Reads backend URL via
import.meta.env.VITE_API_BASE_URL || ''.- If empty, uses same-origin:
fetch('/api/...'),io('').
- If empty, uses same-origin:
- Persists data in localStorage:
- chat message history (
chat-message-history) - agents (
registered-agents) - reading settings (
reading-settings) - last agent per repo (
chat-last-agent-per-repo)
- chat message history (
Backend (../web-backend/)
- Node/Express + Socket.IO server (
server.js). - Executes commands via
spawn('/bin/zsh'|'/bin/bash', ['-l','-c', command]). - Stores state in SQLite via native
sqlite3module; DB path:~/.ahaia/library.db. - Assumes workspace root at
~/workplacefor:- repo scanning (
/api/contacts) - command allowlist check (path must start with workspace)
- markdown file watching (chokidar)
- repo scanning (
- Listens on
0.0.0.0and allows any CORS origin.
Key Findings / Constraints
1) Security: current backend is not safe to expose
As written, if the backend port is reachable from the network, it can become “remote shell execution”. For a desktop app we should:
- bind backend to
127.0.0.1only - require an unguessable per-launch auth token for HTTP and Socket.IO
- tighten/disable broad CORS
2) LocalStorage persistence depends on origin
If the UI is served from http://127.0.0.1:<randomPort>, changing the port changes the origin, which will lose localStorage-based state across launches.
Implication: prefer a stable origin (e.g., Electron app:// protocol), or a stable port.
3) Native module packaging
Backend dependency sqlite3 is native. If we package it inside Electron, we must rebuild it for the Electron runtime during builds.
End users should not compile anything.
4) GUI launch environment on macOS
When launching from Finder, environment variables (including shell init) can differ from Terminal.
Even with zsh -l -c, some users’ toolchains (nvm/asdf/pyenv) may not load if configured only in .zshrc.
We should plan for:
- a configurable shell strategy (
-lvs-lic) - a settings UI to specify extra PATH/env or shell override
Recommendation (Milestone)
Choose Electron first
Electron is the fastest path to a shareable Mac app with an eventual auto-update story.
- Easy to bundle a Node backend and spawn it
- Good tooling:
electron-builder+electron-updater - Can keep the existing Node backend largely intact
Tauri remains a good future option if we want smaller bundles later.
Proposed Architecture (To-Be)
Process Model
- Electron Main: app lifecycle, config, auto-update, spawns backend, workspace picker
- Backend Child Process: Express + Socket.IO + command execution + SQLite
- Renderer (React UI): talks only to
localhostbackend
UI Hosting Strategy (recommended)
- Load UI from stable Electron origin (
app://…viaprotocol.registerFileProtocol)- preserves localStorage across runs
- Backend runs on
127.0.0.1:<port>(port can be dynamic) - Electron preload injects runtime config into the renderer:
apiBaseUrl(e.g.,http://127.0.0.1:8123)authTokenworkspacePath/ config access
Alternative (simpler but less robust): backend serves the UI on a fixed port.
Backend Changes Needed
Configurability
Add env/config support (names TBD):
HOST(default127.0.0.1in packaged app)PORT(default fixed or 0 for ephemeral)WORKSPACE_PATH(instead of~/workplace)DATA_DIR(instead of~/.ahaia)AUTH_TOKEN(required in packaged app)
Authentication
Require token for:
- REST endpoints: header like
Authorization: Bearer <token> - Socket.IO:
io(url, { auth: { token } })and validate inio.use((socket,next)=>...)
Tighten network surface
- Bind to
127.0.0.1by default in desktop mode - Reduce/disable permissive CORS; allow only renderer origin as needed
Path validation hardening
Current startsWith() checks can be fooled by prefix collisions.
Use a safer check:
path.relative(workspaceRoot, target)and reject if it starts with'..'or is absolute
Workspace watching and scanning
- Move chokidar watch root to
WORKSPACE_PATH - Handle non-existent workspace gracefully (no crash)
Data location
Store SQLite and logs under DATA_DIR (Electron app.getPath('userData')).
Frontend Changes Needed
Runtime API base URL
Add a small helper (example contract):
- Prefer
window.__AHAIA__.apiBaseUrlif present (in Electron) - Else fallback to
import.meta.env.VITE_API_BASE_URL || ''(web/dev)
Auth token plumbing
- For
fetch, includeAuthorizationheader - For Socket.IO, pass
auth: { token }
Keep “web mode” working
- Dev server continues using Vite proxy and/or
.env. - Desktop mode uses injected config.
Electron App Outline
Main process responsibilities
- Load/save app config under
userData(JSON) - First-run: prompt “Select workspace folder”
- Spawn backend:
- set
WORKSPACE_PATH,DATA_DIR,AUTH_TOKEN,PORT - wait for
GET /healthsuccess
- set
- Create BrowserWindow:
- load
app://index.html - preload exposes
apiBaseUrl,authToken,selectWorkspace(), etc.
- load
Backend spawn approach
Spawn Electron’s embedded Node:
spawn(process.execPath, ['--runAsNode', backendEntry], { env })
Packaging
- Build React:
vite build - Package Electron app with:
- frontend
dist/ - backend code + production deps
- rebuilt
sqlite3for Electron
- frontend
Distribution + Auto-Update (Later)
Early friend builds
- Ship unsigned DMGs for quick testing (friends may need Gatekeeper “Open Anyway”).
Auto-update target
- Use
electron-updaterwith GitHub Releases or a generic update server. - Requires Apple Developer ID signing + notarization for smooth UX.
Milestone Plan (Checklist)
Phase 0 — Baseline (local dev)
- Run frontend+backend from source reliably using pinned Node (nvm/Volta)
Phase 1 — Electron dev shell
- Add Electron skeleton (main/preload)
- Load React UI in a BrowserWindow
Phase 2 — Bundle backend + local-only security
- Spawn backend from Electron
- Bind backend to
127.0.0.1 - Add
AUTH_TOKENfor HTTP + Socket.IO - Add
WORKSPACE_PATH+ workspace picker - Move DB/logs under Electron
userData
Phase 3 — Package unsigned DMG
-
electron-builderconfig for macOS (arm64/x64 or universal) - Ensure sqlite3 rebuild for Electron
- Smoke-test on a second Mac user account (or another Mac)
Phase 4 — Updates (optional in this milestone)
- Set up GitHub Releases publishing
- Wire
electron-updater(manual update acceptable until signing)
Open Questions
- Should the desktop app support multiple workspace roots or just one active workspace at a time?
- Do we want a fixed backend port (simpler) or dynamic port (avoids conflicts) + stable UI origin?
- Should command execution be limited to repos under the selected workspace only (recommended), or allow arbitrary paths?
- For shell environment: do we need
zsh -licas an option to better load nvm/asdf/pyenv? - Preferred update channel: GitHub Releases, private S3, or something else?