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 |
|---|---|
| Conveyor | Type-safe IPC: queries, commands, streams, events — end-to-end inference |
| Cross-Window Stores | Main-owned state synced live across every window, with opt-in persistence |
| Demo Branch | Live playground of every primitive on the demo branch, main stays minimal |
| Sandboxed Renderer | sandbox: true out of the box — the conveyor preload is sandbox-compatible |
| Custom Titlebar & Menus | Style the window titlebar and menus as you want |
| Clean Project Structure | Separation of main and renderer processes |
| Resources Protocol | Access local file resources via res:// protocol |
| Import Path Aliases | Keep your imports organized and clean |
| Theme Switcher | Built-in theme switching for dark and light mode |
| Error Boundary | Built-in React error boundary with detailed error reporting |
| Code Formatting | Prettier and ESLint pre-configured for code quality |
| Hot Reload | Lightning-fast development with Vite's HMR |
| VS Code Debugging | Pre-configured launch configurations for debugging main and renderer processes |
# Clone the repository
git clone https://github.com/guasam/electron-react-app
# Change directory
cd electron-react-app
# Install dependencies (use any package manager: npm, yarn, pnpm, bun)
npm installnpm run devThis 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 your app on top of it
immediately.
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 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's 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're ready to build.
IPC is powered by electron-conveyor. One definition in main is the single source of truth for a feature; the renderer client is inferred from it — no channel strings, no hand-written API classes, no query keys.
Five primitives:
| You want… | Use | Renderer side |
|---|---|---|
| Read something from main | query() |
await it(), or .useQuery() |
| Tell main to do something | command() |
await it(), or .useMutation() |
| Chunks pushed as they're produced | stream() |
for await, or .useStream() |
| Main pushing to the renderer | event() |
.subscribe(cb), or .useEvent(cb) |
| State shared live across all windows | defineStore |
useConveyorStore(def) |
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] }
)Done — 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().
Every handler receives ctx: the calling window and sender, plus the app context defined in
conveyor/init.ts (this starter kit provides appStartedAt, the windows manager, and
openWindow). Middleware can guard and widen it:
const authed = command.use(requireUser) // a reusable guarded base
export const account = defineModule({
delete: authed(({ ctx }) => deleteAccount(ctx.user.id)),
})// main
respond: stream(z.string(), async function* ({ input, signal }) {
for await (const token of llm.complete(input)) {
if (signal.aborted) return
yield token
}
})
// renderer
for await (const token of conveyor.stream.respond(prompt)) append(token)// main — typed emitters per window or fan-out via the window manager
const emit = createEmitter(windowModule, win)
win.on('focus', () => emit.onFocusChange(true))
// renderer
conveyor.window.onFocusChange.useEvent(setFocused)// conveyor/stores/shared.ts — pure, imported by BOTH processes
export const sharedStore = defineStore('shared', {
state: { count: 0, notes: [] as string[] },
schemas: { add: z.string() }, // payloads validated in main; types flow from the schema
actions: {
add: (s, note) => {
s.notes.push(note)
},
increment: (s) => {
s.count += 1
},
},
persist: true, // survives restarts (JSON under userData)
})
// renderer — feels local, synced across every window
const count = useConveyorStore(sharedStore, (s) => s.count)
const { add, increment } = useConveyorActions(sharedStore)Failures re-throw in the renderer as ConveyorError with a stable code — including custom codes
thrown by your handlers (throw new ConveyorError('LOCKED', '...')). Branch on err.code, never
on message strings. See the demo branch's Middleware page for a working example.
📖 Full API documentation: electron-conveyor
This starter kit includes a custom window implementation with:
- Custom titlebar with app icon
- Window control buttons (minimize, maximize, close)
- Menu system with keyboard shortcuts
- Dark/light mode toggle
- Cross-platform support for Windows and macOS
The titlebar menu can be toggled using:
- Windows: Press the
Altkey - macOS: Press the
Option (⌥)key
When you press the toggle key:
- If the menu is hidden, it becomes visible
- If the menu is already visible, it gets hidden
- The menu only toggles if menu items are available
To add, remove or modify menu items, update the following file:
app/shell/menu.ts
- React application that runs in the browser window
app/shell/— titlebar, menus, window frame, theme
conveyor/init.ts— authoring primitives bound to the app's contextconveyor/modules/— feature modules (main-process only; the renderer imports onlytype AppRouter)conveyor/router.ts— the single registration point (modules, stores, middleware, context)conveyor/client.ts— the typed renderer client with hooks
- Window creation (
app.ts, with the window manager), app lifecycle,res://protocol
- Two lines: expose the conveyor bridge. It never changes as your API grows, and it is
sandbox-compatible — the renderer runs with
sandbox: true
import { Button } from '@/app/components/ui/button'
import { conveyor } from '@/conveyor/client'@/app/→app/(renderer)@/lib/→lib/(main + preload)@/conveyor/→conveyor/(the IPC surface)@/resources/→resources/(build resources)
- UI Development: Work in
app/with React components - IPC: Add a module in
conveyor/modules/, register it inconveyor/router.ts - Window Features: Customize the shell in
app/shell/ - Checks:
npm run typecheck,npm run lint,npm run format
# For Windows
npm run build:win
# For macOS
npm run build:mac
# For Linux
npm run build:linux
# Unpacked for all platforms
npm run build:unpackDistribution files will be located in the dist directory.
