GuidesPacks
Create a Desktop App
Create an Electron desktop app around a TanStack Router web frontend with Tailwind CSS, shadcn/ui, and electron-builder packaging using Better Fullstack.
Use the Desktop App pack when a React web frontend should also ship as a desktop app, with one codebase for the browser and the Electron window.
Generate the desktop starter
npm create better-fullstack@latest my-desktop-app -- \ --ecosystem typescript \ --frontend tanstack-router \ --backend none \ --runtime none \ --database none \ --orm none \ --auth none \ --api none \ --addons electron \ --css-framework tailwind \ --ui-library shadcn-uiWhat the pack includes
- A TanStack Router app in
apps/webbuilt with Vite, with Tailwind CSS, shadcn/ui components, and a theme toggle. - An Electron main process in
apps/web/electron/main.mjsthat opens a 1200 by 800 window with context isolation on and Node integration off. - A preload script in
apps/web/electron/preload.mjsthat callscontextBridge.exposeInMainWorldto exposewindow.desktop.platform. - An
electron-builder.ymlthat packagesdistandelectronintoapps/web/release. - A Vite config with
base: "./", so the built app loads from the file system inside Electron.
Generated shape
my-desktop-app/
apps/web/
electron-builder.yml
electron/
main.mjs
preload.mjs
src/
main.tsx
routes/
__root.tsx
index.tsx
vite.config.ts
packages/
config/
env/Run it locally
Install dependencies, then start the desktop app from apps/web:
npm installcd apps/web
npm run desktop:devdesktop:dev runs the Vite dev server and, once http://localhost:5173 answers, starts Electron with ELECTRON_RENDERER_URL set to that address, so the window loads the dev server. Without that variable, the main process loads dist/index.html. npm run dev in apps/web still runs the plain web app in a browser.
To package the app, run npm run desktop:build from apps/web. It runs vite build and then electron-builder, which writes installers to apps/web/release.
First things to customize
Read the preload API from React with a typed global:
declare global {
interface Window {
desktop?: { platform: string };
}
}
export function PlatformBadge() {
const platform = window.desktop?.platform ?? "web";
return <span>{platform}</span>;
}window.desktop is undefined in a normal browser, so the same component works in both targets.
Then:
- Set
appIdandproductNameinelectron-builder.yml, and add icons and platform targets before shipping. - Add features to the preload script with
contextBridge.exposeInMainWorldandipcRenderer, and handle them in the main process. Keep Node APIs out of the renderer. - If the app needs shared data, generate a separate backend such as Hono and call it over HTTP from the renderer.
Compatibility notes
- The CLI accepts
--addons electrononly with thetanstack-router,react-vite,vanilla-vite,vue, andsolidfrontends. - The preload is an ES module. Electron documents that ESM preload scripts need an unsandboxed renderer, and the generated window does not set
sandbox, so Electron's default applies. Ifwindow.desktopis undefined in the window, setsandbox: falseinwebPreferencesor convert the preload to CommonJS. --addons tauriis the alternative for a Rust-based desktop shell and supports more frontends. Its generatedtauri.conf.jsonlists icon files undersrc-tauri/iconsthat the generator does not write, so create them withtauri iconbefore the first build.
When this is the right pack
Choose this pack when the desktop app is a wrapper around a web UI that also runs in the browser. For a native mobile target instead, read the Mobile App pack, and see the CLI create reference for every flag.