In the previous post, I explained why I built data-peek. Now letβs talk about how I built it.
Choosing a tech stack for a desktop app in 2025 is... interesting. Youβve got Electron, Tauri, Flutter, native frameworks, and a dozen other options. Hereβs what I chose and why.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DESKTOP APPLICATION β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Frontend (Renderer Process) β
β βββ React 19 UI framework β
β βββ TypeScript Type safety β
β βββ Zustand State ...
In the previous post, I explained why I built data-peek. Now letβs talk about how I built it.
Choosing a tech stack for a desktop app in 2025 is... interesting. Youβve got Electron, Tauri, Flutter, native frameworks, and a dozen other options. Hereβs what I chose and why.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DESKTOP APPLICATION β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Frontend (Renderer Process) β
β βββ React 19 UI framework β
β βββ TypeScript Type safety β
β βββ Zustand State management β
β βββ Monaco Code editor β
β βββ TanStack Table Data grid β
β βββ TanStack Router Navigation β
β βββ shadcn/ui Component library β
β βββ Tailwind CSS 4 Styling β
β βββ @xyflow/react ERD visualization β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Backend (Main Process) β
β βββ Electron 38 Desktop runtime β
β βββ pg PostgreSQL client β
β βββ mysql2 MySQL client β
β βββ mssql SQL Server client β
β βββ better-sqlite3 Local storage β
β βββ electron-store Encrypted settings β
β βββ Vercel AI SDK LLM integration β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β Build & Development β
β βββ electron-vite Fast bundling β
β βββ pnpm workspaces Monorepo management β
β βββ Vitest Testing β
β βββ electron-builder Distribution β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Let me walk through the key decisions.
Why Electron?
I know, I know. "Electron apps are bloated." "Use Tauri instead." Iβve heard it all.
Hereβs why I still chose Electron:
1. Mature Ecosystem
Electron has been around since 2013. VS Code, Slack, Discord, Figma (desktop), Notion... all Electron. That means:
- Battle-tested in production at scale
- Extensive documentation
- Solutions for almost every problem on Stack Overflow
- Large ecosystem of tools (electron-builder, electron-store, etc.)
2. Node.js Integration
I need to run database drivers. pg, mysql2, and mssql are Node.js packages. Electron gives me full Node.js in the main process.
With Tauri, Iβd need to:
- Write database connections in Rust
- Bridge Rust to JavaScript
- Maintain two language codebases
For a solo developer, thatβs a non-starter.
3. Development Speed
electron-vite gives me:
- Hot module replacement in the renderer
- Fast rebuilds (~100ms)
- TypeScript out of the box
- Same dev experience as a web app
I can iterate faster with Electron than any alternative.
4. Cross-Platform for Free
One codebase runs on macOS (Intel + Apple Silicon), Windows, and Linux. electron-builder handles code signing, notarization, auto-updates, and installers.
The Size Trade-off
Yes, Electron bundles Chromium. The app is ~150MB. But in 2025:
- Storage is cheap
- Download speeds are fast
- Users donβt care about 150MB (they care about functionality)
Iβd rather ship a working app than a small broken one.
React 19: The UI Layer
React 19 brought some nice improvements:
Concurrent Features
// Suspense for data loading
<Suspense fallback={<SchemaLoader />}>
<SchemaExplorer />
</Suspense>
useTransition for Responsive UI
const [isPending, startTransition] = useTransition()
const handleSearch = (query: string) => {
startTransition(() => {
setSearchResults(filterSchemas(query))
})
}
Large schema lists (1000+ tables) stay responsive during filtering.
Why Not Vue/Svelte/Solid?
Reactβs ecosystem is unmatched for what I needed:
- Monaco React bindings -
@monaco-editor/reactjust works - TanStack - Router and Table are React-first
- shadcn/ui - It is slowly becoming the trivial solution when crafting interfaces.
- Familiarity - Iβve shipped React apps for years
Could I build this in Vue? Sure. But Iβd spend more time on tooling and less on features.
TypeScript: Strict Mode or Bust
Every .ts and .tsx file in data-peek is strictly typed:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true
}
}
Why It Matters
In a database client, type safety prevents disasters:
// Without strict mode, this compiles fine and crashes at runtime
function formatCell(value: unknown) {
return value.toString() // π₯ value might be null
}
// With strict mode, TypeScript forces you to handle it
function formatCell(value: unknown) {
if (value === null) return 'NULL'
if (value === undefined) return ''
return String(value)
}
Shared Types Package
The IPC contract between main and renderer is defined in a shared package:
packages/shared/src/index.ts
export interface Connection {
id: string
name: string
host: string
port: number
database: string
username: string
password: string
dbType: 'postgresql' | 'mysql' | 'mssql'
ssl?: boolean
}
export interface QueryResult {
rows: Record<string, unknown>[]
fields: FieldInfo[]
rowCount: number
duration: number
}
Both processes import from @shared/*. If I change a type, TypeScript catches mismatches at compile timeβnot when a user runs a query.
Zustand: State Management That Doesnβt Hurt
Iβve used Redux, MobX, Recoil, Jotai, and Context. Zustand is my favorite for mid-size apps.
11 Focused Stores
src/renderer/src/stores/
βββ connection-store.ts # Active connection, available connections
βββ query-store.ts # Query history, execution state
βββ tab-store.ts # Editor tabs
βββ edit-store.ts # Pending row edits
βββ ddl-store.ts # Table designer state
βββ ai-store.ts # AI chat sessions
βββ settings-store.ts # User preferences
βββ license-store.ts # License status
βββ saved-queries-store.ts # Bookmarked queries
βββ sidebar-store.ts # Sidebar state
βββ schema-store.ts # Cached schema info
Simple API
// Define a store
export const useConnectionStore = create<ConnectionState>()(
persist(
(set, get) => ({
connections: [],
activeConnection: null,
setActiveConnection: (conn) => set({ activeConnection: conn }),
addConnection: async (conn) => {
const connections = [...get().connections, conn]
set({ connections })
},
}),
{ name: 'connections' }
)
)
// Use it anywhere
function ConnectionSelector() {
const { connections, activeConnection, setActiveConnection } = useConnectionStore()
// ...
}
No providers, no boilerplate, no action creators. Just functions.
Persistence Built-In
persist(
(set, get) => ({ /* ... */ }),
{ name: 'connections' } // Saves to localStorage
)
Query history, saved queries, and settings survive app restarts.
Monaco: VS Codeβs Editor in Your App
Monaco is the editor that powers VS Code. It gives data-peek:
- Syntax highlighting for SQL (PostgreSQL, MySQL, T-SQL)
- Multi-cursor editing
- Find and replace with regex
- Keyboard shortcuts users already know
- Theming that matches the app
Custom Configuration
<MonacoEditor
language="sql"
theme={isDark ? 'vs-dark' : 'vs'}
options={{
minimap: { enabled: false },
lineNumbers: 'on',
fontSize: 14,
tabSize: 2,
wordWrap: 'on',
automaticLayout: true,
}}
onChange={handleQueryChange}
/>
Schema-Aware Autocomplete
Monacoβs completion provider lets me inject schema context:
monaco.languages.registerCompletionItemProvider('sql', {
provideCompletionItems: (model, position) => {
const suggestions = getTableAndColumnSuggestions(currentSchema)
return { suggestions }
}
})
Type users. and see column suggestions. Type FROM and see table names.
shadcn/ui + Tailwind CSS 4
I didnβt want to build a design system from scratch. shadcn/ui provides:
- Copy-paste components - Not a npm dependency, actual source code
- Radix primitives - Accessible by default
- Tailwind styling - Customizable without fighting CSS
Component Examples
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">New Connection</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Add Connection</DialogTitle>
</DialogHeader>
<ConnectionForm onSubmit={handleAdd} />
</DialogContent>
</Dialog>
Tailwind CSS 4
The new version has:
- CSS-first config - No more
tailwind.config.js - Native CSS variables - Better theming
- Smaller output - Only includes used classes
@theme {
--color-primary: oklch(0.7 0.15 250);
--color-background: oklch(0.98 0 0);
--color-foreground: oklch(0.1 0 0);
}
Dark mode is a single class toggle:
<html className={theme === 'dark' ? 'dark' : ''}>
@xyflow/react: ERD Visualization
Building an Entity Relationship Diagram from scratch would take weeks. @xyflow/react (formerly React Flow) gave me:
- Draggable nodes for tables
- Edges for foreign key relationships
- Mini-map for navigation
- Zoom/pan controls
- Custom node components
I added collision detection on top (more on that in a future post).
The Monorepo Structure
data-peek/
βββ apps/
β βββ desktop/ # Electron app
β βββ web/ # License API + marketing site
βββ packages/
β βββ shared/ # Shared TypeScript types
βββ docs/ # Documentation
βββ seeds/ # Test database seeds
βββ pnpm-workspace.yaml
βββ package.json
Why pnpm?
- Fast - Symlinks instead of copying
- Strict - No phantom dependencies
- Workspaces - Native monorepo support
Workspace Commands
# Run dev for desktop only
pnpm --filter @data-peek/desktop dev
# Build all workspaces
pnpm build
# Lint everything
pnpm lint
Build & Distribution
electron-vite
Vite-based bundling for Electron:
// electron.vite.config.ts
export default defineConfig({
main: {
build: { rollupOptions: { external: ['pg', 'mysql2', 'mssql'] } }
},
preload: {
build: { rollupOptions: { external: ['electron'] } }
},
renderer: {
plugins: [react()],
resolve: { alias: { '@': resolve('src/renderer/src') } }
}
})
Hot reload in the renderer, fast rebuilds in main process.
electron-builder
Handles everything for distribution:
# electron-builder.yml
appId: com.datapeek.app
productName: data-peek
mac:
target: [dmg, zip]
hardenedRuntime: true
notarize: true
win:
target: [nsis]
linux:
target: [AppImage, deb, tar.gz]
publish:
provider: github
One command builds for all platforms:
pnpm build:mac
pnpm build:win
pnpm build:linux
What Iβd Do Differently
Consider Wails for v2
If I were starting fresh with more time, Iβd seriously look at Wails. Since it uses Go for the backend, I could leverage my Go experience and avoid the Rust learning curve. The smaller binary size compared to Electron is compelling too.
More Tests from Day One
I added tests late. The SQL builder and parser now have good coverage, but UI tests are sparse.
Conclusion
The tech stack for data-peek prioritizes:
- Developer productivity - Fast iteration, familiar tools
- User experience - Responsive UI, native feel
- Maintainability - Type safety, clear architecture
- Cross-platform - One codebase, three platforms
Is it the "optimal" stack? Probably not. But itβs the stack that let me ship a working product.
Next up: Supporting 3 Databases with One Codebase: The Adapter Pattern