Everything you need to build a serious desktop app with Effect and Electron. A shell that supervises a local Effect server, typed RPC and IPC everywhere, and one React build that runs in the shell and in your browser.
Most Electron starters give you a window and a bundler, then leave the hard parts to you — process supervision, secure IPC, auth, reconnect logic, packaging landmines. This one ships with the hard parts already solved, as Effect programs end to end.
- ⚡️ Effect v4 everywhere — the Electron main process, the server, and the client transport are all Effect programs: Layers, typed errors, scoped cleanup
- 🔒 Typed wires — every RPC and IPC message is an
effect/Schemacontract, validated on both sides - 🛡️ Secure by default — sandboxed renderer, context isolation, secrets passed over fd 3 (never argv or env), bearer auth on the WebSocket
- 🔄 Reconnects that just work — one supervisor, capped backoff, streams re-attach themselves after every blip
- 🌐 Develop in the browser — the same web build runs in the Electron shell and in a plain browser tab with HMR
- 🖥️ A supervised backend — the shell health-checks the server before showing a window, and restarts it with backoff if it dies
- 📦 Packaging solved — electron-builder wiring with the ESM/CJS and Node-version landmines already defused
- 🤖 Agent-ready —
AGENTS.mdconventions and the Effect source vendored in-repo, so coding agents read real code instead of guessing
Requires Node 24 and pnpm 11.
npx degit AdiRishi/electron-effect-starter my-app
cd my-app
pnpm install
pnpm rename my-app com.mycompany # make the starter yours (name, product name, app id, data dir)
pnpm dev # server + web UI in your browser, with HMRpnpm dev:desktop # the real Electron shell
pnpm dist:desktop # build a distributable installer
pnpm check # typecheck + lint + format
pnpm test # vitest across every packageNo .env needed — ports are derived per checkout, so multiple clones never collide.
┌─────────────────────────────┐ spawns · supervises · restarts ┌──────────────────────────┐
│ Electron shell │─────────────────────────────────────▶│ Local Effect server │
│ apps/desktop │ bootstrap token over fd 3 │ apps/server │
│ (an Effect program) │ │ HTTP + WS RPC, loopback │
└──────────────┬──────────────┘ └────────────▲─────────────┘
│ schema-validated IPC bridge │
▼ │
┌─────────────────────────────┐ WebSocket RPC at /ws, bearer-authorized │
│ Renderer — apps/web │───────────────────────────────────────────────────┘
│ (same build also runs in │
│ a plain browser) │
└─────────────────────────────┘
- The shell owns the server. It picks the port, spawns the server, probes its health endpoint, and reveals the window only when the backend answers.
- Trust is minted once. A per-launch token reaches the server over fd 3, gets exchanged for a bearer, and is checked once — at the WebSocket upgrade.
- Exactly one thing reconnects. The connection supervisor is the only retrier; a browser opened before the server starts simply converges on "Connected".
- One build, two hosts. Bridge present → shell. Bridge absent → browser fallbacks. Components never branch on the host.
The docs/ folder has the full story behind every decision.
The UI is a deliberately small notes app that exercises every seam the starter builds: schema-validated mutations (notes.create / update / delete), a snapshot-then-live push-bus subscription (notes.subscribe), atomic persistence in the server's data dir, and typed domain errors. Open pnpm dev in a browser tab and pnpm dev:desktop side by side — a note flashes in every window as each change round-trips through the local server. That's the whole architecture, visible.
It's built to be deleted: the domain lives in exactly four bounded places — packages/contracts/src/notes.ts (+ its rpc.ts entries), apps/server/src/notes/, and apps/web/src/features/notes/. Replace them with your own domain and everything else keeps working.
| Path | What it is |
|---|---|
apps/desktop |
Electron shell: backend supervision, window, menus, updater, IPC bridge |
apps/server |
Effect HTTP + WS RPC server; serves the built web app, owns the auth exchange |
apps/web |
React 19 + Vite + Tailwind UI; the synced-notes sample app |
packages/contracts |
Schema-only wire contracts: RPC surface, IPC bridge, auth. No runtime logic |
packages/client-runtime |
Connection supervisor + typed RPC client (/connection, /rpc, /authorization) |
packages/shared |
Cross-app utilities: port finding, readiness probes, atomic writes. Subpath imports only |
scripts |
Dev runner, desktop packaging, .repos sync — dependency-free where it matters |
docs/adr |
Six short records of every decision you might otherwise be tempted to revisit |
.repos |
Vendored, read-only Effect source for reference — never imported, never edited |
This starter is distilled from T3 Code — the shell-supervised server, the schema contracts, the connection supervisor, even the tooling choices all trace back to patterns it pioneered. If you want to see them driving a real product, go read that codebase. Huge thanks to the T3 team. 🙏