Production-ready desktop app starter. Electron 35 · React 19 · TanStack Router · oRPC · Drizzle · Tailwind v4 · Ship a desktop app in minutes.
A senior-grade Electron monorepo: type-safe IPC over MessagePort, a factory-pattern database with auto-migrations, and a strict CI matrix where every workflow does exactly one thing. Two deployable surfaces (apps/desktop and apps/web) share four packages (api, db, sdk, ui) on a single pnpm workspace.
| Layer | What you get | Why it matters |
|---|---|---|
| Desktop shell | Electron 35, electron-vite, electron-builder, frameless window with hidden title bar | One command boots dev, one command ships a signed-capable installer. |
| Renderer | React 19.2, TanStack Router (file-based SPA, no SSR), TanStack Query 5, Tailwind v4 (CSS-first) | Modern React with type-safe routing, server-state caching, and a theme system that's two CSS variables away. |
| IPC | oRPC over MessagePort, a typed end-to-end contract with no contextBridge API surface |
One source of truth for renderer ↔ main calls; types flow from server to client, no manual DTOs. |
| Database | Drizzle ORM + better-sqlite3 (WAL), factory initDatabase(), auto-migrations at boot |
Testable, parallelizable, no global singleton; migrations apply before the window opens. |
| Type safety | TypeScript ^6, zod ^4 validation at every boundary, shared @electron-template/sdk |
Contract drift is a compile error, not a runtime crash. |
| UI primitives | shadcn/ui (added via pnpm ui:add), Radix UI, lucide-react, sonner, recharts, date-fns |
Bring only what you need; everything lives in packages/ui. |
| i18n | i18next + react-i18next + browser language detection | Render-time locale switching, no async boot delay. |
| Testing | Vitest 4, real SQLite fixtures in packages/db |
Tests run with the production ORM, not an in-memory mock. |
| Tooling | ESLint 10, Prettier, electron-rebuild for native modules | Lint and format are per-workspace, so failures localize. |
| CI | 19 GitHub Actions workflows, one action per workflow, parallel matrix | When a pipeline fails, you know exactly which check failed. |
- Monorepo over polyrepo. Shared types, single install, atomic refactors. The renderer, the IPC server, and the database schema all change in one PR.
- MessagePort IPC over
contextBridge. Same security model (contextIsolation + sandbox + CSP), better performance, no exposed API surface to leak. - Factory database over singleton.
initDatabase({ dataPath, backup })returns a fresh handle. No module-level mutable state means tests can run in parallel withoutSQLITE_BUSYtraps. 127.0.0.1, notlocalhost. Some corporate networks and DNS setups don't resolvelocalhostconsistently. Using the literal IP sidestepsERR_CONNECTION_TIMED_OUT.- Auto-migrations at boot. No separate
db:migratestep to forget in deploy.runMigrations(handle.db)runs as part of app startup. - One workflow per action. 19 separate
.github/workflows/*.ymlfiles. Each runs on its own filter path; failures are easy to attribute. - oRPC AppRouter contract. A single
AppRoutertype inpackages/apidefines every server procedure. The renderer imports the inferred client type, so drift is a compile error.
Prerequisites
- Node.js ≥ 22.13.0 (
engines.nodeenforced in rootpackage.json) - pnpm 9.15+ (
corepack enableif not installed) - Windows 10/11 for the current build target (other platforms are a config line away, see Deployment)
Install and run
# 1. Clone
git clone https://github.com/deessejs/complete-electron-template.git
cd complete-electron-template
# 2. Install (postinstall rebuilds native modules + builds all packages)
pnpm install
# 3. Start the desktop app in dev mode (boots main + preload + renderer)
pnpm dev:desktopThe window opens against the Vite dev server at http://127.0.0.1:5173. Hot-reload works in main, preload, and renderer simultaneously.
Web-only dev
pnpm --filter web dev # standalone SPA on http://127.0.0.1:3456Useful when iterating on UI without the Electron shell.
| Command | Purpose |
|---|---|
pnpm dev:desktop |
Boot Electron (main + preload + renderer) in dev mode |
pnpm build:web |
Build the web renderer into apps/web/dist |
pnpm build:desktop |
Build the Electron app and copy renderer into out/renderer |
pnpm setup |
One-time project scaffolding (scripts/setup.ts) |
pnpm clean |
Remove all build artifacts |
pnpm reset:db |
Wipe the local SQLite database |
pnpm outdated |
List outdated dependencies across the workspace |
pnpm outdated:summary |
Pretty-printed version summary |
pnpm outdated:strict |
Fail if any dependency is behind |
pnpm typecheck:scripts |
Type-check root scripts (tsconfig.scripts.json) |
pnpm ui:add |
Add a shadcn component into packages/ui |
| Command | Purpose |
|---|---|
pnpm --filter desktop dev |
electron-vite dev |
pnpm --filter desktop build |
electron-vite build + copy apps/web/dist into out/renderer |
pnpm --filter desktop preview |
electron-vite preview |
pnpm --filter desktop release |
Build + copy renderer + run electron-builder --win |
pnpm --filter desktop lint |
ESLint the desktop app |
pnpm --filter desktop typecheck |
tsc --noEmit |
pnpm --filter desktop rebuild:native |
Rebuild better-sqlite3 for the bundled Node ABI |
| Command | Purpose |
|---|---|
pnpm --filter web dev |
Vite dev server on port 3456 |
pnpm --filter web build |
Production renderer build |
pnpm --filter web preview |
Serve the production build locally |
pnpm --filter web test |
Vitest run |
pnpm --filter web lint |
ESLint |
pnpm --filter web format |
Prettier write |
pnpm --filter web typecheck |
tsc --noEmit |
| Command | Purpose |
|---|---|
pnpm --filter @electron-template/db clean |
Remove packages/db/dist |
pnpm --filter @electron-template/db build |
Compile + copy Drizzle migrations to dist/drizzle |
pnpm --filter @electron-template/db db:generate |
Diff schema → write SQL migration |
pnpm --filter @electron-template/db db:migrate |
Apply pending migrations |
pnpm --filter @electron-template/db db:push |
Push schema directly (dev only, never in prod) |
pnpm --filter @electron-template/db db:studio |
Open Drizzle Studio in the browser |
pnpm --filter @electron-template/db test |
Vitest with real SQLite fixtures |
Each exposes build, typecheck, lint, and (where applicable) test. The packages/sdk prepare hook runs build automatically, so the renderer can import its types as soon as pnpm install finishes.
None. This template requires no environment variables to run. The SQLite database file is created automatically at <userData>/data/database.sqlite (Windows: %APPDATA%/Complete Electron Template/data/database.sqlite). To override the path, see Customization.
If you later add a remote service or secret, add a Zod-validated loader in packages/api and document it here. Don't reach for process.env directly.
complete-electron-template/
├── apps/
│ ├── desktop/ # Electron shell
│ │ ├── src/
│ │ │ ├── main/ # Main process: app lifecycle, IPC server, DB init
│ │ │ ├── preload/ # MessagePort forwarder (no contextBridge)
│ │ │ └── renderer/ # (loaded from apps/web via electron-vite root)
│ │ ├── electron-builder.json
│ │ └── electron.vite.config.ts
│ └── web/ # Renderer source (React SPA)
│ ├── src/
│ │ ├── routes/ # TanStack Router file-based routes
│ │ ├── components/ # App-level components
│ │ ├── lib/ # oRPC client, query setup, utilities
│ │ └── hooks/
│ └── vite.config.ts
├── packages/
│ ├── api/ # oRPC AppRouter contract + handlers
│ ├── db/ # Drizzle schema + factory + migrations
│ ├── sdk/ # Shared types and client helpers
│ └── ui/ # Reusable UI primitives (shadcn source)
├── scripts/ # Root-level TS scripts
│ ├── setup.ts
│ ├── clean.ts
│ ├── reset-db.ts
│ └── outdated.ts
├── .github/workflows/ # 19 CI workflows
├── CLAUDE.md # Dev guidelines for AI agents
├── LICENSE
└── README.md
Currently ships Windows x64. Other platforms are a config edit away, see the end of this section.
# Build the desktop app and produce a Windows directory artifact
pnpm --filter desktop releaseOutputs land in apps/desktop/release/:
Complete Electron Template-1.0.0.exe, a portable executablewin-unpacked/, an unpacked application directory (runComplete Electron Template.exeinside)
The electron-builder.json target is currently dir with arch: x64. Switch to NSIS for a proper installer, or add mac / linux targets for cross-platform builds.
Edit apps/desktop/electron-builder.json to add targets:
{
"mac": { "target": "dmg", "category": "public.app-category.developer-tools" },
"linux": { "target": "AppImage", "category": "Development" }
}Then run the corresponding electron-builder flag on the host OS that matches the target. Universal macOS binaries use --mac universal. Code signing and notarization are not configured by default. Set them up before your first public release.
dev → staging → main
Promotion is managed by the release-manager process. To cut a release:
git checkout main
git pull
git tag v1.2.3
git push origin v1.2.3The v*.*.* tag triggers .github/workflows/release-desktop.yml, which builds the Windows artifacts and uploads them to the GitHub release.
What ships: out/main/, out/preload/, out/renderer/ (copied from apps/web/dist at build time), packages/db/dist/drizzle/ (SQL migrations).
What doesn't ship yet: macOS .dmg, Linux AppImage, auto-update channel (electron-updater), code signing.
| What | Where | Default | When to change |
|---|---|---|---|
appId |
apps/desktop/electron-builder.json |
com.electron-template.app |
Before your first release. Use your reverse-DNS bundle ID. Changing it after install is breaking (Windows treats it as a different app). |
productName |
apps/desktop/electron-builder.json |
Complete Electron Template |
Display name in the OS taskbar / dock / window list. |
ALLOWED_ORIGIN |
apps/desktop/src/preload/index.ts |
http://127.0.0.1:5173 |
Only if you change the renderer dev port. |
| Database path | apps/desktop/src/main/index.ts |
<userData>/data |
Multi-user installs, portable mode, or CI fixtures. |
| Renderer dev port | apps/desktop/electron.vite.config.ts (server.port) |
5173 |
Only if 5173 is taken on your machine. |
| Build target | apps/desktop/electron-builder.json (win.target, add mac, linux) |
Windows dir x64 |
Switch to NSIS for an installer; add macOS / Linux for cross-platform. |
| shadcn components | pnpm ui:add |
none by default | Add only what you need. They live in packages/ui/src/components/. |
| CI matrix | .github/workflows/ |
19 workflows | Add a new workflow per concern; never combine concerns in one file. |
MessagePort IPC. The preload is a forwarder, not an API surface. It listens for a postMessage('start-orpc-client', port) from the renderer, then transfers the port to the main process via 'start-orpc-server'. Main hosts the oRPC server on that port. The renderer talks to a typed oRPC client over MessagePort; types flow from @electron-template/sdk. There is no contextBridge.exposeInMainWorld.
Factory database, no env vars. initDatabase({ dataPath, backup }) returns a fresh { sqlite, db } handle. No module-level mutable state, no getDb() singleton. Migrations run automatically via runMigrations(handle.db) at boot. Drizzle pragmas: journal_mode = WAL, foreign_keys = ON, synchronous = NORMAL, busy_timeout = 5000.
127.0.0.1, not localhost. The renderer dev server, preload origin check, and CSP connect-src all bind or expect 127.0.0.1. Some networks don't resolve localhost consistently, so the literal IP avoids the timeout.
oRPC AppRouter contract. A single AppRouter type in packages/api defines every server procedure. The renderer imports the inferred client type once, so contract drift is a compile error. Zod schemas validate every input at the boundary.
One workflow per action. 19 separate workflows: build-{api,db,desktop,sdk,web}, lint-{api,db,desktop,sdk} + lint.yml (web), typecheck-{api,db,desktop,sdk} + typecheck.yml (web), test-{api,db,web}, release-desktop.yml. Each runs on its own filter path; failures are easy to attribute and the matrix runs in parallel.
Vite version note. apps/web is on Vite ^8.0.0 (Rolldown bundler). Some legacy Vite plugins may not be compatible yet. The path aliases in electron.vite.config.ts exist partly to work around Rolldown's stricter exports resolution.
CSP. A Content-Security-Policy is installed via webRequest.onHeadersReceived. Production blocks inline scripts; dev allows unsafe-inline + ws://127.0.0.1:5173 for Vite HMR. This is the compensating control for sandbox: false on the BrowserWindow.
This is a template, not a product. Contributions should keep it boring infrastructure, not opinionated features.
- Fork the repo, branch off
dev. - Use conventional commits (
feat(electron): …,fix(db): …,docs(readme): …). - PRs target
dev. Thedev → staging → mainpromotion flow runs separately. - Open an issue before proposing larger changes. Bug reports with reproduction steps are always welcome.
MIT. See LICENSE.