Bootstrap template for building native macOS desktop applications with Electrobun, React 19, Vite 6, TailwindCSS 4, and shadcn/ui.
Ships with TanStack Router (hash-based, file-based routing), TanStack Query (async state over typed RPC), TanStack Table, and a layered backend (controller / service / repository) running on Bun.
# Development with Vite HMR (recommended)
bun run dev:hmr
# Production build
bun run build:stable| Script | Command | Description |
|---|---|---|
start |
vite build && electrobun dev |
Build assets, launch in dev mode |
dev |
electrobun dev --watch |
Launch with file watching (no HMR) |
dev:hmr |
concurrently "vite" "vite build && electrobun dev" |
Vite HMR + Electrobun |
build |
vite build && electrobun build |
Production build |
build:canary |
vite build && electrobun build --env=canary |
Canary channel build |
build:stable |
vite build && electrobun build --env=stable |
Stable channel build |
lint |
oxlint |
Lint (oxlint) |
lint:fix |
oxlint --fix |
Lint + autofix |
fmt |
oxfmt |
Format (oxfmt) |
fmt:check |
oxfmt --check |
Check formatting |
ci |
concurrently "oxlint --fix" "oxfmt" |
Lint + format (CI gate) |
The main process (src/bun/index.ts) checks the Electrobun update channel. In dev channel, it probes http://localhost:5173 on startup. If reachable, the app loads from the Vite dev server with instant HMR. Otherwise it falls back to the bundled views://app/index.html.
bun run dev:hmr-- Starts Vite on port 5173, then builds and launches Electrobun. The BrowserWindow loads from Vite.bun run start-- Builds assets first, then launches Electrobun loading from the bundled HTML. Rebuild to see changes.
SPA with hash-based client-side routing via createHashHistory. Hash routing is required because Electrobun's views:// protocol is filesystem-backed with no server-side rewrite support.
Routes use TanStack Router file-based routing with flat .-separated nesting. Route files live in src/app/routes/. The generated route tree (routeTree.gen.ts) is auto-generated by the Vite plugin and gitignored.
The root layout (__root.tsx) wraps the app with providers:
ThemeProvider-- dark / light / system theme withlocalStoragepersistenceActiveProjectProvider-- tracks selected project across viewsSidebarProvider+TooltipProvider-- shadcn layout primitives
The Bun-side code follows a controller / service / repository pattern:
Controller --> Service --> Repository --> Data Source
Each entity (project, task) has its own file at each layer under src/api/. Repositories currently read from static JSON files in src/lib/data/. Swap these for a database, HTTP calls, or any other data source without changing the layers above.
Bidirectional typed RPC between the browser (React) and main process (Bun) via Electrobun's BrowserView.defineRPC. The type contract lives in src/bun/rpc.ts as the RPC type.
- Browser side:
src/lib/electroview.tsexports a guardedElectroviewinstance (nullwhen running in a plain browser during development). - Bun side:
src/bun/rpc.tsdefines request and message handlers, delegating to controllers.
TanStack Query hooks in src/hooks/queries/ call RPC when the Electrobun runtime is present and fall back to static JSON imports when it is not (browser-only dev).
TanStack Query is configured with networkMode: "always" because RPC is local IPC, not HTTP.
-
Add the type to the
RPCtype insrc/bun/rpc.tsunderbun.requests:getItems: { params: { filter?: string }; response: Item[] };
-
Implement the handler in the same file under
handlers.requests:requests: { getItems: ({ filter }) => itemController.getItems({ filter }), }
-
Build the controller / service / repository chain in
src/api/. -
Create a TanStack Query hook in
src/hooks/queries/:import { electroview } from "@/lib/electroview"; export const itemsQueryOptions = (filter?: string) => queryOptions({ queryKey: ["items", { filter }], queryFn: () => electroview!.rpc.request.getItems({ filter }), enabled: !!electroview, });
src/
api/
controllers/ # Request handlers (thin, delegate to services)
project.controller.ts
task.controller.ts
services/ # Business logic
project.service.ts
task.service.ts
repositories/ # Data access (currently static JSON)
project.repository.ts
task.repository.ts
app/
index.html # SPA HTML shell
index.tsx # React entry (hash router, QueryClient, providers)
routes/
__root.tsx # Root layout (sidebar, providers, outlet)
index.tsx # / -- dashboard (stats, chart, activity, table)
tasks.$taskId.tsx # /tasks/:taskId -- task detail view
bun/
index.ts # Main process (BrowserWindow, HMR detection)
rpc.ts # RPC type contract + handler wiring
components/
dashboard/ # Dashboard-specific components
activity-feed.tsx
project-summary-card.tsx
stats-grid.tsx
task-velocity-chart.tsx
tasks-table.tsx
global/ # App-wide layout components
app-sidebar.tsx
nav-main.tsx
nav-projects.tsx
nav-user.tsx
page-header.tsx
tasks/ # Task detail view components
task-activity.tsx
task-back-link.tsx
task-description.tsx
task-meta-card.tsx
task-sidebar-info.tsx
task-subtasks.tsx
ui/ # 55 shadcn/ui components
hooks/
queries/ # TanStack Query options (RPC + JSON fallback)
activity.ts
projects.ts
tasks.ts
team.ts
use-mobile.ts # Responsive breakpoint hook
lib/
context/
active-project.tsx # Active project context + provider
theme-provider.tsx # Theme context (dark/light/system)
data/ # Static JSON seed data
activity.json
projects.json
tasks.json
team.json
types/ # TypeScript types organized by entity
activity.ts
project.ts
task.ts
team.ts
electroview.ts # Browser-side RPC singleton (guarded)
format.ts # Label formatting + badge variant maps
utils.ts # cn() -- clsx + tailwind-merge
styles/
global.css # Tailwind CSS + theme tokens
electrobun.config.ts # App metadata, build, copy rules
vite.config.ts # Vite + TanStack Router plugin + Tailwind
components.json # shadcn/ui configuration
Routes -- Add files to src/app/routes/. Use . for nesting (e.g., settings.profile.tsx mounts at /settings/profile).
Components -- shadcn/ui components live in src/components/ui/. Add more:
bunx shadcn@latest add <component>Theme -- Edit CSS variables in src/styles/global.css.
Window -- Edit src/bun/index.ts (title, size, titlebar style, transparency).
App metadata -- Edit electrobun.config.ts.
Lint / Format -- Uses oxlint + oxfmt (not eslint / prettier). Config in .oxlintrc.json and .oxfmtrc.json.
| Layer | Technology |
|---|---|
| Runtime | Electrobun 1.15 + Bun |
| UI Framework | React 19 |
| Routing | TanStack Router (file-based, hash history) |
| State | TanStack Query |
| Tables | TanStack Table |
| Components | shadcn/ui (Radix + Base UI primitives) |
| Styling | TailwindCSS 4 + tw-animate-css |
| Icons | Lucide React |
| Charts | Recharts |
| Build | Vite 6 |
| Language | TypeScript 5.7 |
| Lint / Format | oxlint + oxfmt |
