
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.
What's Included
| 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. |
Why This Template
- 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 without SQLITE_BUSY traps.
127.0.0.1, not localhost. Some corporate networks and DNS setups don't resolve localhost consistently. Using the literal IP sidesteps ERR_CONNECTION_TIMED_OUT.
- Auto-migrations at boot. No separate
db:migrate step to forget in deploy. runMigrations(handle.db) runs as part of app startup.
- One workflow per action. 19 separate
.github/workflows/*.yml files. Each runs on its own filter path; failures are easy to attribute.
- oRPC AppRouter contract. A single
AppRouter type in packages/api defines every server procedure. The renderer imports the inferred client type, so drift is a compile error.
Quick Start
Prerequisites
- Node.js ≥ 22.13.0 (
engines.node enforced in root package.json)
- pnpm 9.15+ (
corepack enable if not installed)
- Windows 10/11 for the current build target (other platforms are a config line away, see Deployment)
Install and run
The 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:3456
Useful when iterating on UI without the Electron shell.
Available Commands
Root
| 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 |
|
apps/desktop
| 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 |
apps/web
| 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 |
|
packages/db
| 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) |
|
packages/api, packages/sdk, packages/ui
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.
Environment Variables
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.
Project Structure
Deployment
Currently ships Windows x64. Other platforms are a config edit away, see the end of this section.
Local release build
# Build the desktop app and produce a Windows directory artifact
pnpm --filter desktop release
Outputs land in apps/desktop/release/:
Complete Electron Template-1.0.0.exe, a portable executable
win-unpacked/, an unpacked application directory (run Complete Electron Template.exe inside)
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.
Adding macOS and Linux
Edit apps/desktop/electron-builder.json to add targets:
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.
GitHub release flow
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.3
The 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.
Customization
| 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 |
Architecture Notes
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.
Contributing
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. The dev → staging → main promotion flow runs separately.
- Open an issue before proposing larger changes. Bug reports with reproduction steps are always welcome.
License
MIT. See LICENSE.