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

Practical Electron + Prisma Integration Guide

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

Introduction

This guide walks you through building a real-world task management system using Electron and Prisma. We'll cover everything from initial setup to handling complex edge cases, using a practical example that you can follow along with.

Example System: TaskMaster Pro

We'll build a task management application that works offline, syncs when online, and handles multiple windows efficiently. This example will demonstrate all the key concepts in a practical context.

System Requirements

  • Offline-first task management
  • Real-time sync between windows
  • Data persistence across app updates
  • Efficient handling of large task lists
  • Crash recovery and data integrity

1. Database Schema & Models

First, let's define our data model. We'll use a practical task management schema:

// prisma/schema.prisma
datasource db {
  provider = "sqlite"
  url      = "file:./taskmaster.db"
}
 
generator client {
  provider = "prisma-client-js"
  engineType = "binary" // Optimized for desktop
}
 
model Task {
  id          String    @id @default(cuid())
  title       String
  description String?
  status      TaskStatus
  priority    Priority
  dueDate     DateTime?
  tags        Tag[]
  project     Project?  @relation(fields: [projectId], references: [id])
  projectId   String?
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
  syncStatus  SyncStatus @default(PENDING)
}
 
model Project {
  id          String    @id @default(cuid())
  name        String
  tasks       Task[]
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
}
 
model Tag {
  id          String    @id @default(cuid())
  name        String    @unique
  tasks       Task[]
}
 
enum TaskStatus {
  TODO
  IN_PROGRESS
  COMPLETED
}
 
enum Priority {
  LOW
  MEDIUM
  HIGH
}
 
enum SyncStatus {
  PENDING
  SYNCED
  CONFLICT
}

Why This Schema?

  1. Offline-First: The syncStatus field helps manage offline/online synchronization
  2. Relationships: Demonstrates one-to-many (Project-Tasks) and many-to-many (Tasks-Tags) relationships
  3. Enums: Shows how to handle fixed-value fields properly in Electron
  4. Timestamps: Crucial for sync conflict resolution

2. Database Setup & Initialization

Here's how we handle database setup in an Electron context:

// src/main/database/setup.ts
import { app } from 'electron';
import path from 'path';
import { PrismaClient } from '@prisma/client';
import { DatabaseMigrator } from './migrator';
 
export class DatabaseSetup {
  private static instance: DatabaseSetup;
  private prisma: PrismaClient;
  private readonly dbPath: string;
 
  private constructor() {
    this.dbPath = this.resolveDatabasePath();
    this.prisma = this.createPrismaClient();
  }
 
  private resolveDatabasePath(): string {
    // In development, use a local database
    if (process.env.NODE_ENV === 'development') {
      return path.join(process.cwd(), 'prisma/taskmaster.db');
    }
    
    // In production, store in user's app data
    return path.join(app.getPath('userData'), 'taskmaster.db');
  }
 
  private createPrismaClient(): PrismaClient {
    process.env.DATABASE_URL = `file:${this.dbPath}`;
    
    return new PrismaClient({
      log: process.env.NODE_ENV === 'development' 
        ? ['query', 'error', 'warn']
        : ['error'],
      errorFormat: 'minimal',
    });
  }
 
  async initialize(): Promise<void> {
    try {
      // Ensure database exists and is migrated
      const migrator = new DatabaseMigrator(this.dbPath);
      await migrator.ensureDatabase();
      
      // Test connection
      await this.prisma.$connect();
      
      console.log('Database initialized successfully');
    } catch (error) {
      console.error('Failed to initialize database:', error);
      throw error;
    }
  }
 
  static getInstance(): DatabaseSetup {
    if (!DatabaseSetup.instance) {
      DatabaseSetup.instance = new DatabaseSetup();
    }
    return DatabaseSetup.instance;
  }
}

Key Points About Database Setup

  1. Path Resolution:

    • Development: Uses local database for easy debugging
    • Production: Stores in user's app data directory
    • Why? Ensures proper permissions and data persistence
  2. Singleton Pattern:

    • Why use it? Prevents multiple database connections
    • When to create? At app startup
    • How to access? Through getInstance()

3. IPC Communication Layer

Let's build a type-safe IPC layer for database operations:

// src/shared/ipc/types.ts
export interface TaskOperation<T = any> {
  type: 'CREATE' | 'UPDATE' | 'DELETE' | 'READ';
  model: 'Task' | 'Project' | 'Tag';
  data?: T;
  where?: Record<string, unknown>;
}
 
// src/shared/ipc/channels.ts
export const IPC_CHANNELS = {
  TASK: {
    OPERATION: 'task:operation',
    SYNC: 'task:sync',
    STATUS: 'task:status'
  }
} as const;

Now, let's implement the IPC handler:

// src/main/ipc/task-handler.ts
import { ipcMain } from 'electron';
import { IPC_CHANNELS } from '@shared/ipc/channels';
import { TaskOperation } from '@shared/ipc/types';
import { DatabaseSetup } from '../database/setup';
 
export class TaskIpcHandler {
  private db: PrismaClient;
 
  constructor() {
    this.db = DatabaseSetup.getInstance().getPrismaClient();
    this.setupHandlers();
  }
 
  private setupHandlers(): void {
    ipcMain.handle(
      IPC_CHANNELS.TASK.OPERATION,
      async (_event, operation: TaskOperation) => {
        try {
          return await this.handleOperation(operation);
        } catch (error) {
          console.error('Task operation failed:', error);
          throw error;
        }
      }
    );
  }
 
  private async handleOperation(operation: TaskOperation): Promise<any> {
    const { type, model, data, where } = operation;
 
    switch (type) {
      case 'CREATE':
        return this.db[model.toLowerCase()].create({ data });
      
      case 'UPDATE':
        return this.db[model.toLowerCase()].update({
          where,
          data
        });
      
      case 'DELETE':
        return this.db[model.toLowerCase()].delete({
          where
        });
      
      case 'READ':
        return this.db[model.toLowerCase()].findMany({
          where,
          include: this.getIncludes(model)
        });
      
      default:
        throw new Error(`Unknown operation type: ${type}`);
    }
  }
 
  private getIncludes(model: string): Record<string, boolean> {
    // Define relationships to include based on model
    const includes: Record<string, Record<string, boolean>> = {
      Task: {
        project: true,
        tags: true
      },
      Project: {
        tasks: true
      }
    };
 
    return includes[model] || {};
  }
}

Why This IPC Structure?

  1. Type Safety:

    • All operations are typed
    • Prevents runtime errors from invalid operations
    • Enables better IDE support
  2. Centralized Handling:

    • Single point of database access
    • Consistent error handling
    • Easy to add middleware (logging, validation, etc.)

4. Practical Usage Example

Here's how you'd use this system in a React component:

// src/renderer/components/TaskList.tsx
import { useQuery, useMutation } from '@tanstack/react-query';
import { Task } from '@shared/types';
 
export function TaskList() {
  // Fetch tasks
  const { data: tasks, isLoading } = useQuery({
    queryKey: ['tasks'],
    queryFn: async () => {
      return window.electron.invoke(IPC_CHANNELS.TASK.OPERATION, {
        type: 'READ',
        model: 'Task',
        where: {
          status: 'TODO'
        }
      });
    }
  });
 
  // Create task mutation
  const createTask = useMutation({
    mutationFn: async (newTask: Partial<Task>) => {
      return window.electron.invoke(IPC_CHANNELS.TASK.OPERATION, {
        type: 'CREATE',
        model: 'Task',
        data: newTask
      });
    },
    onSuccess: () => {
      // Invalidate and refetch
      queryClient.invalidateQueries({ queryKey: ['tasks'] });
    }
  });
 
  if (isLoading) return <div>Loading tasks...</div>;
 
  return (
    <div className="task-list">
      {tasks.map(task => (
        <TaskItem 
          key={task.id} 
          task={task} 
          onStatusChange={handleStatusChange} 
        />
      ))}
      
      <button
        onClick={() => createTask.mutate({
          title: 'New Task',
          status: 'TODO',
          priority: 'MEDIUM'
        })}
      >
        Add Task
      </button>
    </div>
  );
}

5. Handling Edge Cases

Offline Support

// src/renderer/hooks/useOfflineTask.ts
import { useOfflineStore } from '@/store/offline';
 
export function useOfflineTask() {
  const { addPendingOperation, processPendingOperations } = useOfflineStore();
  const isOnline = useOnlineStatus();
 
  const createTask = async (task: Partial<Task>) => {
    try {
      if (isOnline) {
        // Direct operation
        return await window.electron.invoke(
          IPC_CHANNELS.TASK.OPERATION,
          {
            type: 'CREATE',
            model: 'Task',
            data: task
          }
        );
      } else {
        // Store for later
        addPendingOperation({
          type: 'CREATE',
          model: 'Task',
          data: task
        });
        
        // Return optimistic result
        return {
          ...task,
          id: `temp_${Date.now()}`,
          syncStatus: 'PENDING'
        };
      }
    } catch (error) {
      console.error('Failed to create task:', error);
      throw error;
    }
  };
 
  return { createTask };
}

Real-world Considerations

  1. Data Integrity:

    • Always validate data before operations
    • Use transactions for related changes
    • Handle sync conflicts gracefully
  2. Performance:

    • Implement pagination for large lists
    • Cache frequently accessed data
    • Batch updates when possible
  3. Error Recovery:

    • Log all operations
    • Implement retry mechanisms
    • Provide data export/import

6. Testing Strategy

Here's how to test this system effectively:

// tests/integration/task-operations.test.ts
import { TestContext } from './test-context';
import { TaskOperation } from '@shared/ipc/types';
 
describe('Task Operations', () => {
  let context: TestContext;
 
  beforeEach(async () => {
    context = await TestContext.create();
  });
 
  afterEach(async () => {
    await context.cleanup();
  });
 
  it('should handle concurrent task creation', async () => {
    const operations: TaskOperation[] = Array(5).fill(null).map((_, i) => ({
      type: 'CREATE',
      model: 'Task',
      data: {
        title: `Task ${i}`,
        status: 'TODO',
        priority: 'MEDIUM'
      }
    }));
 
    const results = await Promise.all(
      operations.map(op => 
        context.ipc.invoke(IPC_CHANNELS.TASK.OPERATION, op)
      )
    );
 
    expect(results).toHaveLength(5);
    expect(results.every(r => r.id)).toBe(true);
  });
});

Next Steps

Consider implementing:

  1. Sync System:

    • Real-time updates between windows
    • Conflict resolution
    • Background sync
  2. Performance Monitoring:

    • Query timing
    • Memory usage
    • Operation queuing
  3. Advanced Features:

    • Task templates
    • Batch operations
    • Data export/import

Let me know if you'd like me to expand on:

  • 📊 Database schema visualization
  • 🔍 Advanced query patterns
  • 🏗️ Window management strategies
  • 🔒 Security best practices
  • 🚀 Performance optimization techniques

Webcam Tester

Build a Webcam Testing Component in a Next.js App Router app with the following capabilities, UI behaviors, and data model. It should function independently but follow the same structure as the microphone testing feature.

Complete Electron + Prisma Integration Guide

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

On this page

IntroductionExample System: TaskMaster ProSystem Requirements1. Database Schema & ModelsWhy This Schema?2. Database Setup & InitializationKey Points About Database Setup3. IPC Communication LayerWhy This IPC Structure?4. Practical Usage Example5. Handling Edge CasesOffline SupportReal-world Considerations6. Testing StrategyNext Steps
Jun 23, 2025
2 min read
302 words