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