A modern Electron starter kit with React, Vite, TypeScript and TailwindCSS, built around electron-conveyor for type-safe IPC and cross-window state.
🔹 Electron - Cross-platform desktop application framework.
🔹 React - The library for web and native user interfaces.
🔹 electron-conveyor - Type-safe IPC + cross-window state.
🔹 TypeScript - Type-safe JavaScript.
🔹 Shadcn UI - Beautiful and accessible component library.
🔹 TailwindCSS - Utility-first CSS framework.
🔹 Electron Vite - Lightning-fast build tool based on Vite for fastest hot-reload.
🔹 Electron Builder - Configured for packaging applications.
| Feature | Description |
|---|---|
| Typed IPC | Queries, commands, streams and events with end-to-end inference. No channel strings, no hand-written API classes. |
| Cross-window stores | Main-owned state synced live across every window, with opt-in persistence. |
| Sandboxed renderer | sandbox: true out of the box. The conveyor preload is sandbox-compatible. |
| Custom window shell | Themed frame, titlebar and menu system you can style however you want. |
| Demo branch | A live playground of every primitive on demo, while main stays minimal. |
| The usual tooling | Vite HMR, Shadcn UI, Tailwind, ESLint and Prettier, VS Code debug configs, res:// protocol, import aliases. |
git clone https://github.com/guasam/electron-react-app
cd electron-react-app
# any package manager works: npm, yarn, pnpm, bun
npm install
npm run devThat starts Electron with hot reload. main is deliberately minimal (a themed window frame,
titlebar, menus and the typed IPC layer) so you can start building on top of it right away.
The window opens on a short tour of the stack. It is the one piece meant to be thrown away, and it
is built so that removing it costs nothing: it lives entirely in app/components/welcome, nothing
else imports it, and it adds no IPC modules of its own.
rm -rf app/components/welcomeThen drop the <Welcome /> line and its import from app/app.tsx. What is left is an empty window
with the shell still around it, ready for your app.
Want to see everything the stack can do first? The demo branch is an interactive playground of
every IPC primitive (cross-window state, streaming, background tasks, middleware), with the real
source behind each demo:
git switch demo
npm install
npm run devSwitch back to main and re-run npm install when you are ready to build.
IPC is powered by electron-conveyor. One definition in main is the single source of truth for a feature, and the renderer client is inferred from it.
| You want | Use | Renderer side |
|---|---|---|
| Read something from main | query() |
await conveyor.x.y() or .useQuery() |
| Tell main to do something | command() |
await conveyor.x.y() or .useMutation() |
| Chunks pushed as they're produced | stream() |
for await (const c of conveyor.x.y()) or .useStream() |
| Main pushing to the renderer | event() |
conveyor.x.y.subscribe(cb) or .useEvent(cb) |
| State shared live across windows | defineStore() |
useConveyorStore(store) |
1. Define the module in conveyor/modules/:
// conveyor/modules/notes.ts — runs in MAIN only
import { z } from 'zod'
import { defineModule, query, command } from '../init'
export const notesModule = defineModule({
list: query(() => readNotes()),
// input crosses the trust boundary → schema required, validated on every call
save: command(z.object({ title: z.string(), body: z.string() }), ({ input }) => saveNote(input)),
})2. Register it in conveyor/router.ts:
export const router = createRouter(
{
window: windowModule,
web: webModule,
notes: notesModule, // ← the key becomes the module id
},
{ createContext, use: [devLogger] }
)That's it. The renderer client already knows it, fully typed:
import { conveyor } from '@/conveyor/client'
function Notes() {
const notes = conveyor.notes.list.useQuery() // key derived from the path — never hand-written
const save = conveyor.notes.save.useMutation({
onSuccess: () => conveyor.notes.list.invalidate(),
})
return <button onClick={() => save.mutate({ title: 'Hi', body: '...' })}>Save</button>
}Outside React, every member is a plain typed call: await conveyor.notes.list().
Failures re-throw in the renderer as ConveyorError with a stable code. Conveyor reserves
UNKNOWN_PROCEDURE, INVALID_INPUT, INVALID_OUTPUT and HANDLER_ERROR; anything your handler
throws deliberately keeps its own code:
// main
throw new ConveyorError('LOCKED', 'Unlock the vault first')
// renderer
try {
await conveyor.vault.open()
} catch (err) {
if (err instanceof ConveyorError && err.code === 'LOCKED') promptUnlock()
}Branch on err.code, never on message strings. Validation failures also carry err.issues with
the Standard Schema detail, so you can map them onto form fields. The demo branch's
Middleware page has a working example.
Streams, Events, Middleware and the full Conveyor API reference live in the electron-conveyor repo, and our
demobranch has a working example for each one.
The starter kit ships a custom window implementation: titlebar with app icon, window controls, a menu system with keyboard shortcuts, and a dark/light toggle. It works on Windows, macOS and Linux.
The titlebar menu toggles with Alt on Windows and Linux, Option (⌥) on macOS. Edit the items in
app/shell/menu.ts.
The React application that runs in the browser window. app/shell/ holds the titlebar, menus,
window frame and theme.
init.ts- authoring primitives bound to the app's contextmodules/- feature modules (main-process only; the renderer imports onlytype AppRouter)router.ts- the single registration point for modules, stores, middleware and contextclient.ts- the typed renderer client with hooks
Window creation (app.ts, with the window manager), app lifecycle, and the res:// protocol.
An import and one call that expose the conveyor bridge. It never changes as your API grows, and it
is sandbox-compatible, so the renderer can run with sandbox: true.
import { Button } from '@/app/components/ui/button'
import { conveyor } from '@/conveyor/client'| Alias | Points to |
|---|---|
@/app/ |
app/ (renderer) |
@/lib/ |
lib/ (main + preload) |
@/conveyor/ |
conveyor/ (the IPC surface) |
@/resources/ |
resources/ (build resources) |
npm run typecheck
npm run lint
npm run formatnpm run build:win # Windows
npm run build:mac # macOS
npm run build:linux # Linux
npm run build:unpack # unpacked, all platformsDistribution files land in the dist directory.
