Total Posts

0

Total Commits

0

(v1: 0, v2: 0)
Total Deployments

0

Latest commit:Unable to fetch commit info
6/23/2025
Latest deployment:
pending
6/23/2025
v2
Started 6/23/2025

Built by Remco Stoeten with a little ❤️

Snippets.remcostoeten
Snippets.remcostoeten
Snippets
Welcome to Snippets
Disable Sudo Password Prompts on macOS
Disable Sudo Password Prompts on macOS
Validate env variables
Drizzle ORM Schema Design
Setup Drizzle ORM with SQLite
Keybindings remap
Keyboard Tester Feature Prompt
Microphone Tester Feature Prompt
Webcam Tester
Practical Electron + Prisma Integration Guide
Complete Electron + Prisma Integration Guide
Git Branch Diverged
Git Set Upstream
Text Formatting Components
Suspense Wrapper Guide for SSR and Client UX
Features/Electron snippets

Complete Electron + Prisma Integration Guide

Comprehensive A-Z guide for integrating Prisma with Electron, covering edge cases, IPC communication, and database management

🏗️ Architecture Overview

// src/types/app-architecture.ts
interface AppArchitecture {
  main: {
    database: PrismaClient
    ipc: ElectronIpcMain
    services: MainProcessServices
  }
  renderer: {
    api: ApiClient
    store: AppStore
    ui: ReactComponents
  }
  preload: {
    api: ExposedApi
    bridge: IpcBridge
  }
}

Process Separation

// src/main/index.ts
class MainProcess {
  private window: BrowserWindow
  private prisma: PrismaClient
  private services: ServiceRegistry
 
  constructor() {
    this.initializePrisma()
    this.setupIpcHandlers()
    this.createWindow()
  }
 
  private async initializePrisma() {
    this.prisma = new PrismaClient({
      log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error']
    })
    await this.prisma.$connect()
  }
}

🗄️ Database Setup

SQLite for Production

// prisma/schema.prisma
datasource db {
  provider = "sqlite"
  url      = "file:./data.db"
}
 
generator client {
  provider = "prisma-client-js"
  // Enable native bindings for better performance
  engineType = "binary"
}

Database Location Handling

// src/main/database/path-resolver.ts
import { app } from 'electron'
import path from 'path'
 
export function getDatabasePath(): string {
  const userDataPath = app.getPath('userData')
  return process.env.NODE_ENV === 'development'
    ? path.join(process.cwd(), 'prisma/data.db')
    : path.join(userDataPath, 'data.db')
}
 
// Usage in main process
const dbPath = getDatabasePath()
process.env.DATABASE_URL = `file:${dbPath}`

🔌 IPC Communication

Type-Safe IPC Channels

// src/shared/ipc/channels.ts
export const IPC_CHANNELS = {
  DATABASE: {
    QUERY: 'db:query',
    MUTATION: 'db:mutation',
    TRANSACTION: 'db:transaction',
    SYNC: 'db:sync'
  },
  APP: {
    READY: 'app:ready',
    ERROR: 'app:error',
    UPDATE: 'app:update'
  }
} as const
 
type IpcChannels = typeof IPC_CHANNELS

Preload API Bridge

// src/preload/api-bridge.ts
import { contextBridge, ipcRenderer } from 'electron'
import type { DatabaseOperations } from '../shared/types'
 
export const api = {
  database: {
    query: async <T>(operation: DatabaseOperations, args?: unknown): Promise<T> => {
      return ipcRenderer.invoke(IPC_CHANNELS.DATABASE.QUERY, {
        operation,
        args
      })
    },
 
    // Watch for database changes
    subscribe: (callback: (event: DatabaseEvent) => void) => {
      const unsubscribe = ipcRenderer.on(IPC_CHANNELS.DATABASE.SYNC, callback)
      return () => unsubscribe()
    }
  }
}
 
contextBridge.exposeInMainWorld('electron', api)

🔐 Security & Permissions

Query Validation

// src/main/services/query-validator.ts
import { z } from 'zod'
 
const QuerySchema = z.object({
  operation: z.enum(['findMany', 'findUnique', 'create', 'update', 'delete']),
  model: z.enum(['User', 'Post', 'Comment']),
  args: z.record(z.unknown()).optional()
})
 
export function validateQuery(query: unknown): boolean {
  try {
    QuerySchema.parse(query)
    return true
  } catch (error) {
    console.error('Invalid query:', error)
    return false
  }
}

Content Security Policy

// src/main/window/security.ts
export function setupSecurityPolicy(window: BrowserWindow): void {
  window.webContents.session.webRequest.onHeadersReceived((details, callback) => {
    callback({
      responseHeaders: {
        ...details.responseHeaders,
        'Content-Security-Policy': [
          "default-src 'self'",
          "script-src 'self'",
          "style-src 'self' 'unsafe-inline'",
          "img-src 'self' data: https:"
        ].join('; ')
      }
    })
  })
}

📡 Data Synchronization

Real-time Updates

// src/main/services/sync-service.ts
export class DatabaseSyncService {
  private subscribers: Set<BrowserWindow> = new Set()
 
  constructor(private prisma: PrismaClient) {
    this.setupMiddleware()
  }
 
  private setupMiddleware() {
    this.prisma.$use(async (params, next) => {
      const result = await next(params)
 
      if (params.action !== 'findMany') {
        this.notifySubscribers({
          model: params.model,
          action: params.action,
          data: result
        })
      }
 
      return result
    })
  }
 
  private notifySubscribers(event: DatabaseEvent) {
    this.subscribers.forEach((window) => {
      if (!window.isDestroyed()) {
        window.webContents.send(IPC_CHANNELS.DATABASE.SYNC, event)
      }
    })
  }
}

Offline Support

// src/renderer/store/offline-store.ts
import { createJSONStorage, persist } from 'zustand/middleware'
 
interface OfflineState {
  pendingOperations: DatabaseOperation[]
  addOperation: (op: DatabaseOperation) => void
  processPendingOperations: () => Promise<void>
}
 
export const useOfflineStore = create<OfflineState>()(
  persist(
    (set, get) => ({
      pendingOperations: [],
      addOperation: (operation) =>
        set((state) => ({
          pendingOperations: [...state.pendingOperations, operation]
        })),
      processPendingOperations: async () => {
        const { pendingOperations } = get()
        for (const operation of pendingOperations) {
          try {
            await window.electron.database.query(operation.type, operation.params)
          } catch (error) {
            console.error('Failed to process operation:', error)
          }
        }
        set({ pendingOperations: [] })
      }
    }),
    {
      name: 'offline-store',
      storage: createJSONStorage(() => localStorage)
    }
  )
)

⚡ Performance Optimization

Connection Pooling

// src/main/database/connection.ts
export class DatabaseConnectionManager {
  private static instance: PrismaClient
  private static connectionCount = 0
 
  static async getInstance(): Promise<PrismaClient> {
    if (!this.instance) {
      this.instance = new PrismaClient({
        datasources: {
          db: {
            url: getDatabasePath()
          }
        },
        // Optimize for desktop app usage
        log: ['error'],
        errorFormat: 'minimal',
        connectionLimit: 5
      })
 
      await this.instance.$connect()
    }
 
    this.connectionCount++
    return this.instance
  }
 
  static async releaseInstance(): Promise<void> {
    this.connectionCount--
    if (this.connectionCount === 0) {
      await this.instance.$disconnect()
      this.instance = null
    }
  }
}

Query Optimization

// src/main/services/query-optimizer.ts
export class QueryOptimizer {
  static optimizeQuery(model: string, args: any): any {
    // Implement select optimization
    if (args.select === undefined && args.include === undefined) {
      args.select = this.getDefaultSelection(model)
    }
 
    // Implement pagination
    if (args.take === undefined && !args.where?.id) {
      args.take = 50
    }
 
    return args
  }
 
  private static getDefaultSelection(model: string): Record<string, boolean> {
    const selections: Record<string, Record<string, boolean>> = {
      User: {
        id: true,
        email: true,
        name: true,
        createdAt: false,
        updatedAt: false
      }
      // Add more models...
    }
 
    return selections[model] || {}
  }
}

🐛 Edge Cases & Solutions

Database Locking

// src/main/database/lock-handler.ts
export class DatabaseLockHandler {
  private lockQueue: Array<() => Promise<void>> = []
  private isLocked = false
 
  async acquireLock<T>(operation: () => Promise<T>, timeout = 5000): Promise<T> {
    if (this.isLocked) {
      return new Promise((resolve, reject) => {
        const timeoutId = setTimeout(() => {
          reject(new Error('Database lock timeout'))
        }, timeout)
 
        this.lockQueue.push(async () => {
          clearTimeout(timeoutId)
          try {
            resolve(await operation())
          } catch (error) {
            reject(error)
          }
        })
      })
    }
 
    this.isLocked = true
    try {
      return await operation()
    } finally {
      this.isLocked = false
      this.processQueue()
    }
  }
 
  private async processQueue(): Promise<void> {
    const next = this.lockQueue.shift()
    if (next) {
      await next()
    }
  }
}

Process Crashes

// src/main/error/crash-handler.ts
export class CrashHandler {
  constructor(
    private window: BrowserWindow,
    private prisma: PrismaClient
  ) {
    this.setupHandlers()
  }
 
  private setupHandlers() {
    // Handle renderer process crashes
    this.window.webContents.on('crashed', async () => {
      await this.cleanup()
      this.restartRenderer()
    })
 
    // Handle main process crashes
    process.on('uncaughtException', async (error) => {
      console.error('Uncaught exception:', error)
      await this.cleanup()
      app.quit()
    })
  }
 
  private async cleanup() {
    try {
      await this.prisma.$disconnect()
    } catch (error) {
      console.error('Failed to disconnect Prisma:', error)
    }
  }
 
  private restartRenderer() {
    this.window.loadURL(process.env.VITE_DEV_SERVER_URL)
  }
}

Migration Handling

// src/main/database/migrator.ts
import { execSync } from 'child_process'
import { app } from 'electron'
import path from 'path'
 
export class DatabaseMigrator {
  private readonly migrationPath: string
 
  constructor() {
    this.migrationPath = path.join(app.getPath('userData'), 'migrations')
  }
 
  async migrate(): Promise<void> {
    try {
      // Copy migration files to user data directory
      this.copyMigrationFiles()
 
      // Run migrations
      execSync(`prisma migrate deploy --schema=${this.migrationPath}/schema.prisma`)
    } catch (error) {
      console.error('Migration failed:', error)
      throw new Error('Failed to migrate database')
    }
  }
 
  private copyMigrationFiles() {
    // Implementation to copy migration files from app resources
    // to user data directory
  }
}

📦 Deployment & Distribution

Database Bundling

// electron-builder.config.js
module.exports = {
  extraResources: [
    {
      from: 'prisma',
      to: 'prisma',
      filter: ['*.prisma', 'migrations/**/*']
    }
  ]
  // ... other config
}

First-Run Setup

// src/main/setup/first-run.ts
export class FirstRunSetup {
  constructor(private dbPath: string) {}
 
  async perform(): Promise<void> {
    if (await this.isFirstRun()) {
      await this.setupDatabase()
      await this.createInitialData()
      await this.markSetupComplete()
    }
  }
 
  private async isFirstRun(): Promise<boolean> {
    return !existsSync(this.dbPath)
  }
 
  private async setupDatabase(): Promise<void> {
    const migrator = new DatabaseMigrator()
    await migrator.migrate()
  }
 
  private async createInitialData(): Promise<void> {
    const prisma = await DatabaseConnectionManager.getInstance()
    // Create initial data...
    await DatabaseConnectionManager.releaseInstance()
  }
 
  private async markSetupComplete(): Promise<void> {
    // Save setup completion marker
  }
}

🧪 Testing & Debugging

Integration Tests

// tests/integration/database.test.ts
import { TestContext } from './test-context'
 
describe('Database Integration', () => {
  let context: TestContext
 
  beforeEach(async () => {
    context = await TestContext.create()
  })
 
  afterEach(async () => {
    await context.cleanup()
  })
 
  it('should handle concurrent operations', async () => {
    const operations = Array(10)
      .fill(null)
      .map(() =>
        context.prisma.user.create({
          data: {
            email: `user${Date.now()}@test.com`,
            name: 'Test User'
          }
        })
      )
 
    const results = await Promise.all(operations)
    expect(results).toHaveLength(10)
  })
})

Debug Logging

// src/main/utils/logger.ts
import { app } from 'electron'
import path from 'path'
import winston from 'winston'
 
export const logger = winston.createLogger({
  level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
  format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
  transports: [
    new winston.transports.File({
      filename: path.join(app.getPath('userData'), 'logs', 'error.log'),
      level: 'error'
    }),
    new winston.transports.File({
      filename: path.join(app.getPath('userData'), 'logs', 'combined.log')
    })
  ]
})
 
if (process.env.NODE_ENV === 'development') {
  logger.add(
    new winston.transports.Console({
      format: winston.format.simple()
    })
  )
}

Let me know if you'd like me to expand on any of these sections or add:

  • 📊 Database schema visualization
  • 🔍 Query optimization patterns
  • 🏗️ Additional architectural patterns
  • 🔒 Security hardening strategies
  • 🚀 Performance profiling guide

Practical Electron + Prisma Integration Guide

A practical, example-driven guide for integrating Prisma with Electron, featuring a real-world task management system

Git Branch Diverged

Are you also frustrated when that 'do you want to rebase, merge or else' question pops up?

On this page

🏗️ Architecture OverviewProcess Separation🗄️ Database SetupSQLite for ProductionDatabase Location Handling🔌 IPC CommunicationType-Safe IPC ChannelsPreload API Bridge🔐 Security & PermissionsQuery ValidationContent Security Policy📡 Data SynchronizationReal-time UpdatesOffline Support⚡ Performance OptimizationConnection PoolingQuery Optimization🐛 Edge Cases & SolutionsDatabase LockingProcess CrashesMigration Handling📦 Deployment & DistributionDatabase BundlingFirst-Run Setup🧪 Testing & DebuggingIntegration TestsDebug Logging
Jun 23, 2025
2 min read
302 words