Desktop App · macOS
Zoe
Status
Released
Stack
[ THE THESIS ]
Some tools are better when they never ask the internet for permission.
Zoe packages a Python download engine and an Electron interface into one signed macOS app so Christian sermons and worship media can be saved, organized, and kept offline. The architecture hides the complexity of two runtimes behind one native experience, with local Socket.IO as the bridge instead of accounts, ads, or tracking.
Released · signed + notarized for macOS · fully offline
SECTION 01 — WHY DESKTOP
yt-dlp needs the filesystem and a real process — that's not a browser
The web can't do this job. A desktop architecture with two runtimes glued at the socket layer is the only correct one.
Offline-first
Nothing leaves the machine. No telemetry, no accounts, no analytics. Sermons and worship media stay on the user's disk.
Python owns downloads · JS owns UI
yt-dlp is the best-in-class downloader and it's Python. Electron is the best-in-class cross-platform UI and it's JS. Each runtime does what it's good at.
The user sees one app
Electron boots the Python binary as a hidden child process at launch. Python exits when Electron does. One dock icon, one window, one experience.
Shipped like native software
Code-signed with a Developer ID certificate, notarized by Apple, stapled, bundled as .dmg. Installs through Gatekeeper with zero warnings.
SECTION 02 — TWO-RUNTIME ARCHITECTURE
Electron shell · Python engine · Socket.IO bridge
BrowserWindow
UI renderer
Child process
spawns zoe-engine
Socket.IO client
localhost only
↕ Socket.IO · ws://127.0.0.1:PORT · bound to loopback
Flask HTTP
local control endpoints
Socket.IO server
live progress events
yt-dlp
filesystem + ffmpeg
SECTION 03 — BOOT SEQUENCE
From double-click to ready in five steps
- 01
Electron main launches
app.whenReady() fires. Before opening any window, we resolve the path to the bundled zoe-engine binary inside the .app.
- 02
Spawn the Python engine as a child
spawn(enginePath, [...], { stdio: 'ignore' }) — hidden, detached from the terminal. Stored on a module singleton so we can kill it on quit.
- 03
Wait for the Socket.IO handshake
Electron connects to ws://127.0.0.1:PORT and waits for the engine's 'ready' event. Until it arrives, the UI shows a spinner.
- 04
Open the BrowserWindow
Renderer mounts the React UI. Download requests emit over the socket; progress events stream back in real time.
- 05
Clean shutdown
On 'before-quit', Electron sends a shutdown event and kills the child. Gatekeeper sees a well-behaved app with no orphaned processes.
SECTION 04 — ENGINEERING HIGHLIGHTS
Three decisions that make this feel native, not hacky
PACKAGING
PyInstaller single-file binary inside the .app
The Python runtime, yt-dlp, Flask, and Socket.IO are all frozen into one binary shipped inside Zoe.app/Contents/Resources. The user installs Zoe, not a Python environment.
→ 1 installer · 0 dependencies
IPC
Socket.IO bound to loopback — not just a port
The engine only accepts connections from 127.0.0.1 on a dynamically chosen port. No remote attack surface. No accidental exposure if the machine's firewall drops.
→ local-only IPC by construction
TRUST
Developer ID signed + notarized
Signed with a Developer ID certificate, submitted to Apple's notary service, ticket stapled. Installs on any modern macOS through Gatekeeper with no 'unidentified developer' scare.
→ zero Gatekeeper warnings
SECTION 05 — CODE PROOF
The child-process + socket handshake that makes it feel like one app
main.ts
typescript
// main.ts — Electron boot · spawn Python engine, wait for ready
import { app, BrowserWindow } from "electron";
import { spawn, ChildProcess } from "node:child_process";
import path from "node:path";
import { io } from "socket.io-client";
let engine: ChildProcess | null = null;
function enginePath(): string {
return app.isPackaged
? path.join(process.resourcesPath, "zoe-engine")
: path.join(__dirname, "../python/dist/zoe-engine");
}
async function boot() {
engine = spawn(enginePath(), ["--port", String(PORT)], {
stdio: "ignore",
detached: false,
});
await new Promise<void>((resolve, reject) => {
const sock = io(`http://127.0.0.1:${PORT}`, { reconnectionAttempts: 20 });
sock.once("ready", () => resolve());
sock.once("connect_error", reject);
});
const win = new BrowserWindow({ width: 1100, height: 720 });
await win.loadFile("index.html");
}
app.whenReady().then(boot);
app.on("before-quit", () => {
if (engine && !engine.killed) engine.kill("SIGTERM");
});