Library / SDK
guasam/electron-react-app avatar
guasam/electron-react-app

This Electron starter kit makes the IPC layer the centre of the design

Modern desktop application starter kit with Electron, React, TypeScript, TailwindCSS & Shadcn/UI to build cross-platform apps.

797 stars144 forksTypeScriptMIT

At a glance

What is it?
electron-react-app is a starter kit for cross platform desktop applications built on Electron, React, Vite, TypeScript, Tailwind and shadcn/ui, but its distinguishing feature is electron-conveyor, a typed IPC layer where one definition in the main process is the single source of truth and the renderer client is inferred from it. It is MIT licensed, at version 13.0.0, and its main branch stays deliberately minimal.
Who is it for?
electron-react-app fits a team starting a desktop application that will grow past a single window, because the typed IPC layer removes the most tedious part of Electron development, the stringly typed channel between the main and renderer processes, and the demo branch gives a working reference for every primitive.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The stack, and the one dependency that matters

The README describes the project as a modern Electron starter kit with React, Vite and TypeScript, built around electron-conveyor for type-safe IPC and a smoother development experience.

The version badges give the concrete numbers. Electron 43.5.1, React 19.2.8, TypeScript 6.0.3, Vite 7.3.6, Tailwind 4.3.3 and Conveyor 0.4.0. The package itself is published under the name era, at version 13.0.0, and the main entry point is the built main process file at out/main/main.js.

Electron Conveyor is the piece that makes this different from the many Electron and React templates on GitHub. It is a separate project by the same author, and the README says that IPC is powered by it, that one definition in main is the single source of truth for a feature, and that the renderer client is inferred from it.

Everything else in the feature list is the familiar set. There is Electron for cross platform desktop apps on Windows, macOS and Linux, React, TypeScript across main, preload and renderer, shadcn/ui components built on Radix, TailwindCSS, electron-vite as the build tool, and electron-builder for packaging. There is a custom window shell with a themed frame, titlebar and menus with keyboard shortcuts, a dark and light mode, fast hot module replacement where the renderer updates instantly and the main and preload processes restart on save, an error boundary, a resources protocol that lets the renderer load local files through res://, preconfigured ESLint and Prettier, and VS Code launch configurations for debugging the main and renderer processes.

The cross-window state feature is the other one worth singling out. Stores are owned by the main process, synced live to every window, with persistence as an opt in.

Cloning it and starting it takes three commands

The quick start is the whole installation procedure, and it is short because the project is a template rather than a service to deploy.

bash
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 dev

The dev script sets ELECTRON_DISABLE_SANDBOX=1 and runs electron-vite in watch mode, and the postinstall script installs the Electron binary and the native dependencies for electron-builder. The typecheck step is worth knowing about early, because it runs two separate compiler passes, one against tsconfig.node.json and one against tsconfig.web.json, which reflects the split between the main process and the renderer.

There is a deliberate design decision behind all of this, and the README states it after the quick start. The main branch is minimal, containing a themed window frame, a titlebar, menus and the typed IPC layer, so that you can start building on top of it right away.

That is a different philosophy from a template that ships a full application with routing, state management, a database layer and a service directory. The trade is that you get less for free and you have to decide the architecture of your application yourself, but you also do not have to delete a framework's opinions to get started. The build and packaging scripts are still there for the end, with separate targets for Windows, macOS and Linux that each run the vite build and then electron-builder.

The welcome screen is built to be thrown away

The application opens on a short tour of the stack, and the README explains how to get rid of it. The point of the design is that removing it costs nothing.

bash
rm -rf app/components/welcome

The welcome screen lives entirely in app/components/welcome, nothing else imports it, and it adds no IPC modules of its own. After removing the directory you drop the Welcome element and its import from app/app.tsx, and what is left is an empty window with the shell still around it, ready for your application.

That constraint, the component being self contained and adding no IPC, is what makes the deletion safe. It is a small piece of design discipline that a lot of starter kits lack, and it is a useful signal about the rest of the codebase.

What remains is still a working application rather than an empty void. The window shell, the titlebar with the application icon, the window controls, the menu system with keyboard shortcuts and the dark and light toggle are all part of the shell, and the shell is what the main branch is for. The titlebar menu toggles with Alt on Windows and Linux and with the Option key on macOS, and the shell works on all three platforms.

The demo branch is a working reference for every IPC primitive

Rather than describing the API in prose, the project ships a branch you can run.

bash
git switch demo
npm install
npm run dev

The demo branch is an interactive playground of every IPC primitive, and the README names them: cross-window state, streaming, background tasks and middleware, with the real source behind each demo. When you are done, you switch back to main and run npm install again.

The reason to run it is that the API surface is not small. The features list names queries, commands, streams and events, plus stores, plus a custom window shell, plus hot reload, plus an error boundary, and the README only sketches each of them. A playground turns that list into something you can click.

The branch split also solves a real problem. The main branch is meant to be minimal, and a minimal branch is a poor place to ship fifteen worked examples. Keeping the playground on its own branch means the template you start from stays small, and the reference implementation is one git switch away when you need it.

Conveyor: one definition in main, an inferred client in the renderer

The IPC section is the technical core of the starter, and it works in one direction.

The table in the README maps what you want to what you call. To read something from main you use a query, and the renderer calls await conveyor.x.y() or the useQuery() hook. To tell main to do something you use a command, called as await conveyor.x.y() or through useMutation(). For chunks pushed as they are produced you use a stream, consumed with for await (const c of conveyor.x.y()) or the useStream() hook. For main pushing to the renderer you use an event, subscribed with conveyor.x.y.subscribe(cb) or useEvent(cb). For state shared live across windows you use defineStore() and read it with useConveyorStore(store).

Adding a feature takes two edits. The first is a module in conveyor/modules/, which runs in the main process only.

ts
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)),
})

The second is registering it in the router, where the key becomes the module id.

ts
export const router = createRouter(
  {
    window: windowModule,
    web: webModule,
    notes: notesModule, // ← the key becomes the module id
  },
  { createContext, use: [devLogger] }
)

The renderer then gets a fully typed client with no hand written keys.

ts
import { conveyor } from '@/conveyor/client'

function Notes() {
  const notes = conveyor.notes.list.useQuery()
  const save = conveyor.notes.save.useMutation({
    onSuccess: () => conveyor.notes.list.invalidate(),
  })

  return <button onClick={() => save.mutate({ title: 'Hi', body: '...' })}>Save</button>
}

The two things to notice are the schema on the command, which is required because the input crosses the trust boundary and is validated on every call, and the fact that the key is derived from the path rather than typed by hand, so a rename in main breaks the build instead of breaking at run time. Outside React, every member is a plain typed call, for example await conveyor.notes.list().

Errors carry stable codes, not message strings

The error boundary section describes a design choice that is easy to copy and easy to get wrong without a guide.

Failures re-throw in the renderer as a ConveyorError with a stable code. Conveyor reserves four codes, UNKNOWN_PROCEDURE, INVALID_INPUT, INVALID_OUTPUT and HANDLER_ERROR, and anything your own handler throws deliberately keeps its own code.

ts
// 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()
}

The instruction is explicit: branch on the code, never on message strings. A message can be reworded, translated, or improved by a contributor, and any code that matches on it breaks silently at the worst moment.

Validation failures carry err.issues with the Standard Schema detail, so they can be mapped onto form fields, and the demo branch has a working example on its Middleware page.

The README also points out where the rest of the API lives, with streams, events, middleware and the full reference in the electron-conveyor repository, and the demo branch has an example of each. That split tells you something about the project boundaries, and it is a sensible one: the transport layer is a library, and this repository is the application template that uses it.

Where this starter kit is the wrong choice

Three situations rule it out.

The first is a single window utility. If the application is one window, one view and no cross-window state, then the conveyor layer is a lot of abstraction for one function call across the process boundary, and the custom window shell, the error boundary and the cross-window stores are all unused. A plain Electron and React setup is smaller and easier to reason about.

The second is a team that already has an IPC design. Some applications have a hard requirement for a particular transport, for example a socket the renderer connects to directly, or an existing protocol shared with other processes. The conveyor abstraction is not a thin layer you can hide, because it decides how modules are declared, how the router is built, and how the renderer client is generated, and a project with its own model will end up fighting it.

The third is a team that intends to avoid TypeScript in the renderer or the main process. The type safety is the point of the typed IPC layer, and the two separate typecheck configurations reflect the split between the Node side and the web side. A mixed JavaScript and TypeScript codebase gets less from this template than a plain Vite and React one.

For a team that wants a typed, cross window desktop application with a polished shell and a hot reload loop, the combination is coherent: the strictness in the IPC layer is what makes the rest of the starter worth using.

Editorial conclusion

electron-react-app fits a team starting a desktop application that will grow past a single window, because the typed IPC layer removes the most tedious part of Electron development, the stringly typed channel between the main and renderer processes, and the demo branch gives a working reference for every primitive. It is the wrong choice for a small utility with one window, and the wrong choice if you intend to write your own IPC layer, because the conveyor abstraction shapes how the whole application is structured. Before you adopt it, run the demo branch and read the error handling section, because the stable error codes are the part of the API your application will end up depending on.

Frequently asked questions

How do I start an application with the electron-react-app starter?

Clone the repository, change into the directory, run npm install and then npm run dev, which starts Electron with hot reload. The README notes that any package manager works, including npm, yarn, pnpm and bun.

What makes the electron-conveyor IPC layer different?

One definition in the main process is the single source of truth for a feature, and the renderer client is inferred from it, so channel keys are never hand written. It covers queries, commands, streams, events and cross window stores, with commands taking a schema that is validated on every call.

How do I remove the welcome screen from the Electron starter?

Delete the app/components/welcome directory, which nothing else imports and which adds no IPC modules of its own, then remove the Welcome element and its import from app/app.tsx. What is left is an empty window with the themed shell still in place.

How should I handle errors thrown across the IPC boundary?

Catch the ConveyorError in the renderer and branch on err.code rather than on the message string, since Conveyor reserves UNKNOWN_PROCEDURE, INVALID_INPUT, INVALID_OUTPUT and HANDLER_ERROR and your own codes are preserved. Validation failures also carry err.issues with the Standard Schema detail.

Where can I see working examples of the Electron starter's IPC features?

The demo branch is an interactive playground of every IPC primitive, with the real source behind each demo, and can be run with git switch demo followed by npm install and npm run dev. The README also points to the electron-conveyor repository for the full API reference.

Official sources

  1. guasam/electron-react-app on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/guasam-electron-react-app.svg)](https://hysenlabs.com/projects/guasam-electron-react-app)

Was this useful?