DEV Community

Cover image for The Tech Stack Behind data-peek: Modern Desktop Development in 2025
Rohith Gilla
Rohith Gilla Subscriber

Posted on

The Tech Stack Behind data-peek: Modern Desktop Development in 2025

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                         │
└─────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

useTransition for Responsive UI

const [isPending, startTransition] = useTransition()

const handleSearch = (query: string) => {
  startTransition(() => {
    setSearchResults(filterSchemas(query))
  })
}
Enter fullscreen mode Exit fullscreen mode

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/react just 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
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

Shared Types Package

The IPC contract between main and renderer is defined in a shared package:

packages/shared/src/index.ts
Enter fullscreen mode Exit fullscreen mode
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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
  // ...
}
Enter fullscreen mode Exit fullscreen mode

No providers, no boilerplate, no action creators. Just functions.

Persistence Built-In

persist(
  (set, get) => ({ /* ... */ }),
  { name: 'connections' } // Saves to localStorage
)
Enter fullscreen mode Exit fullscreen mode

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}
/>
Enter fullscreen mode Exit fullscreen mode

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 }
  }
})
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Dark mode is a single class toggle:

<html className={theme === 'dark' ? 'dark' : ''}>
Enter fullscreen mode Exit fullscreen mode

@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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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') } }
  }
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

One command builds for all platforms:

pnpm build:mac
pnpm build:win
pnpm build:linux
Enter fullscreen mode Exit fullscreen mode

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:

  1. Developer productivity - Fast iteration, familiar tools
  2. User experience - Responsive UI, native feel
  3. Maintainability - Type safety, clear architecture
  4. 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

Web
Github

Top comments (0)