← Back

Desktop App · macOS

Zoe

Status

Released

Stack

ElectronPythonFlaskSocket.IOyt-dlp

[ 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

Technical proof
2
runtimes, 1 app
Electron + Python
0
accounts · 0 ads
nothing leaves disk
1
local WebSocket
IPC over Socket.IO
1
signed bundle
notarized for Gatekeeper

SECTION 01WHY 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.

01

Offline-first

Nothing leaves the machine. No telemetry, no accounts, no analytics. Sermons and worship media stay on the user's disk.

02

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.

03

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.

04

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 02TWO-RUNTIME ARCHITECTURE

Electron shell · Python engine · Socket.IO bridge

Electron Main Process · Node runtime

BrowserWindow

UI renderer

Child process

spawns zoe-engine

Socket.IO client

localhost only

↕ Socket.IO · ws://127.0.0.1:PORT · bound to loopback

Python engine · packaged as single binary via PyInstaller

Flask HTTP

local control endpoints

Socket.IO server

live progress events

yt-dlp

filesystem + ffmpeg

SECTION 03BOOT SEQUENCE

From double-click to ready in five steps

  1. 01

    Electron main launches

    app.whenReady() fires. Before opening any window, we resolve the path to the bundled zoe-engine binary inside the .app.

  2. 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.

  3. 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.

  4. 04

    Open the BrowserWindow

    Renderer mounts the React UI. Download requests emit over the socket; progress events stream back in real time.

  5. 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 04ENGINEERING 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 05CODE 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");
});