본문으로 건너뛰기

데이터베이스

Elftia는 better-sqlite3 + Drizzle ORM을 로컬 데이터베이스 솔루션으로 사용합니다. 모든 데이터베이스 작업은 Worker Thread를 통해 비동기적으로 실행됩니다.

기술 선택

항목선택이유
데이터베이스 엔진better-sqlite3임베디드, 무설정, 고성능
ORMDrizzle ORM타입 안전, 런타임 오버헤드 없음, SQL 유사 API
모드WAL (Write-Ahead Logging)동시 읽기 지원, 쓰기가 읽기를 차단하지 않음
접근 패턴Worker Thread RPC메인 스레드 차단 방지

데이터베이스 위치

{userData}/elftia.db # 메인 데이터베이스 파일
{userData}/elftia.db-wal # WAL 로그
{userData}/elftia.db-shm # 공유 메모리 파일

플랫폼별 {userData} 경로:

  • Windows: %APPDATA%/elftia/
  • macOS: ~/Library/Application Support/elftia/
  • Linux: ~/.config/elftia/

Worker Thread 아키텍처

모든 데이터베이스 작업은 DbClient -> db.worker.ts RPC 패턴을 통해 실행됩니다:

sequenceDiagram
participant S as Service Layer
participant C as DbClient (main thread)
participant W as db.worker.ts (Worker Thread)
participant DB as SQLite (WAL)

Note over S,DB: Initialization
C->>W: Create Worker({dbPath})
W->>DB: new Database(dbPath)
W->>DB: PRAGMA journal_mode = WAL
W->>DB: drizzle(db) + migrate()
W-->>C: ready signal

Note over S,DB: Normal operation
S->>C: db.chatSessionsList()
C->>C: id = ++seq
C->>W: postMessage({id, method: 'chatSessions:list', params})
W->>DB: SELECT * FROM chat_sessions ...
DB-->>W: Result rows
W-->>C: postMessage({id, result: rows})
C->>C: pending[id].resolve(rows)
C-->>S: Promise<Session[]>

DbClient 핵심 구현

{/* packages/desktop/app/main/workers/DbClient.ts */}
class DbClient extends EventEmitter implements DbRpc {
private worker: Worker;
private seq = 0;
private pending = Map<number, { resolve, reject }>;

constructor(dbPath: string, options?: DbClientOptions) {
this.worker = new Worker(workerPath, {
workerData: { dbPath, diagnosticsDir },
});
this.worker.on('message', (msg) => this.handleMessage(msg));
}

async ready(): Promise<void>;

private async send(method: string, params: unknown): Promise<unknown> {
const id = ++this.seq;
return new Promise((resolve, reject) => {
this.pending.set(id, { resolve, reject });
this.worker.postMessage({ id, method, params });
});
}

// 100+ typed methods
chatSessionsList(): Promise<ChatSession[]>;
chatMessagesInsert(msg: ChatMessageInput): Promise<void>;
auditLogInsert(entry: AuditLogEntry): Promise<void>;
}

RPC 타입 정의

{/* packages/desktop/app/main/workers/types.ts */}
interface DbRequest {
id: number;
method: string;
params: unknown;
}

interface DbResponse {
id: number;
result?: unknown;
error?: { message: string; stack?: string };
}

interface DbRpc {
chatSessionsList(): Promise<ChatSession[]>;
chatSessionsGet(id: string): Promise<ChatSession | null>;
chatSessionsCreate(input: ChatSessionInput): Promise<ChatSession>;
chatSessionsUpdate(id: string, data: Partial<ChatSession>): Promise<void>;
chatSessionsDelete(id: string): Promise<void>;
// ... 100+ methods
}

스키마 구성

데이터베이스 스키마는 Drizzle ORM으로 정의되어 있으며, packages/desktop/app/main/db/schema/ 디렉터리에 위치합니다:

packages/desktop/app/main/db/schema/
├── index.ts # 통합 내보내기
├── accounts.ts # 사용자 계정
├── chat.ts # 채팅 메시지
├── sessions.ts # 채팅 세션
├── llm.ts # LLM 공급자/모델/키
├── projects.ts # 프로젝트
├── settings.ts # 애플리케이션 설정
├── attachments.ts # 첨부 파일
├── media.ts # 미디어 리소스
└── templates.ts # 템플릿

핵심 데이터 테이블

채팅 관련

{/* Simplified table structure definitions */}

// 채팅 세션
interface ChatSession {
id: string; // UUID
title: string; // 세션 제목
type: 'chat' | 'roleplay' | 'agent';
agentId?: string; // 연결된 Agent ID
personaId?: string; // 연결된 Persona ID
providerId?: string; // LLM 공급자 ID
modelId?: string; // 모델 ID
rpConfig?: RPConfig; // RP 설정 (JSON)
createdAt: number;
updatedAt: number;
lastMessageAt?: number;
messageCount: number;
pinned: boolean;
folderId?: string;
}

// 채팅 메시지
interface ChatMessage {
id: string; // UUID
sessionId: string; // 소속 세션 ID
role: 'user' | 'assistant' | 'system';
content: string; // 메시지 내용
parentId?: string; // 부모 메시지 ID (브랜치 지원)
reasoning?: string; // 추론 내용 (thinking)
toolCalls?: ToolCall[]; // 도구 호출 (JSON)
meta?: MessageMeta; // 메타데이터 (providerId, modelId, tokenCount)
createdAt: number;
}

계정 관련

// 사용자 계정
interface Account {
id: string;
email: string;
displayName?: string;
avatarUrl?: string;
createdAt: number;
}

// 계정 토큰
interface AccountToken {
id: string;
accountId: string;
provider: string; // OAuth 공급자
accessToken: string; // 암호화 저장
refreshToken?: string;
expiresAt?: number;
}

LLM 설정

// LLM 공급자
interface LLMProvider {
id: string;
name: string;
type: string; // openai, anthropic, google 등
apiKey?: string; // 암호화 저장
baseUrl?: string; // 커스텀 API 엔드포인트
enabled: boolean;
models: string[]; // 사용 가능한 모델 목록
}

// API 키 풀 (다중 키 지원)
interface ApiKeyEntry {
id: string;
providerId: string;
label?: string;
apiKey: string; // 암호화 저장
weight: number; // 라운드 로빈 가중치
enabled: boolean;
createdAt: number;
}

프로젝트 관련

// 프로젝트
interface Project {
id: string;
name: string;
path: string; // 파일시스템 경로
description?: string;
createdAt: number;
updatedAt: number;
}

보안 감사

// 감사 로그 항목
interface AuditLogEntry {
id: string;
timestamp: number;
eventType: string; // tool_executed, injection_detected 등
severity: 'info' | 'warning' | 'critical';
channelId?: string;
userId?: string;
toolName?: string;
details: string; // JSON 직렬화된 상세 정보
}

미디어 및 리소스

// 미디어 리소스
interface MediaResource {
id: string;
sessionId: string;
messageId: string;
filePath: string;
mimeType: string;
thumbnailPath?: string;
size: number;
createdAt: number;
}

// 첨부 파일
interface Attachment {
id: string;
sessionId: string;
messageId: string;
name: string;
type: string;
url?: string;
size: number;
}

기타 핵심 테이블

테이블목적스키마 파일
settings애플리케이션 설정 (키-값)schema/settings.ts
templates이미지 템플릿schema/templates.ts
channel_instances채널 인스턴스Worker db/channels.ts
channel_users채널 사용자Worker db/channelUsers.ts
character_cards캐릭터 카드Worker db/characterCards.ts
world_info_booksWorld Info 책Worker db/worldInfo.ts
world_info_entriesWorld Info 항목Worker db/worldInfo.ts
custom_agents커스텀 AgentWorker db/customAgents.ts
personasPersona 정의Worker db/personas.ts
user_skills사용자 SkillWorker db/userSkills.ts
tags태그Worker db/tags.ts
notes노트 인덱스Worker db/notes.ts
note_folders노트 폴더Worker db/noteFolders.ts
cron_jobs정기 작업CronService 파일시스템

DB Worker 모듈

데이터베이스 작업의 구체적인 구현은 packages/desktop/app/main/workers/db/ 아래 여러 모듈에 분산되어 있습니다:

workers/db/
├── index.ts # 메인 디스패처; method 접두사에 따라 적절한 모듈로 라우팅
├── chatSessions.ts # chatSessions:* 메서드
├── chatMessages.ts # chatMessages:* 메서드
├── customAgents.ts # customAgents:* 메서드
├── personas.ts # personas:* 메서드
├── characterCards.ts # characterCards:* 메서드
├── worldInfo.ts # worldInfo:* 메서드
├── groupChat.ts # groupChat:* 메서드
├── channels.ts # channels:* 메서드
├── channelUsers.ts # channelUsers:* 메서드
├── auditLog.ts # auditLog:* 메서드
├── apiKeys.ts # apiKeys:* 메서드
├── tags.ts # tags:* 메서드
├── notes.ts # notes:* 메서드
├── noteFolders.ts # noteFolders:* 메서드
├── userSkills.ts # userSkills:* 메서드
├── rpDefaults.ts # rpDefaults:* 메서드
└── characterSprites.ts # sprites:* 메서드

마이그레이션 시스템

Drizzle 마이그레이션

스키마 변경사항은 Drizzle의 migrate()를 통해 자동으로 적용됩니다:

{/* packages/desktop/app/main/db/index.ts */}
function initDatabase() {
const sqlite = new Database(dbPath);
sqlite.pragma('journal_mode = WAL');
sqlite.pragma('foreign_keys = ON');
const db = drizzle(sqlite);
migrate(db, { migrationsFolder: 'drizzle' });
}

커스텀 마이그레이션

복잡한 데이터 마이그레이션 로직은 migrations.ts에 구현됩니다:

{/* Custom migration example */}
async function migrateApiKeyPool(db: DbRpc) {
const providers = await db.llmProvidersList();
for (const p of providers) {
if (p.apiKey) {
await db.apiKeysInsert({
providerId: p.id,
apiKey: p.apiKey,
weight: 1,
enabled: true,
});
}
}
}

초기화 시퀀스

sequenceDiagram
participant M as Main Process
participant C as DbClient
participant W as db.worker.ts
participant D as SQLite

M->>C: new DbClient(dbPath)
C->>W: Create Worker Thread
W->>D: new Database(dbPath)
W->>D: PRAGMA journal_mode = WAL
W->>D: PRAGMA foreign_keys = ON
W->>W: Register all RPC methods
W-->>C: ready signal

M->>C: await db.ready()
Note over M: Worker 초기화 완료 후 진행

M->>M: initDatabase()
Note over M: Drizzle ORM 초기화 + migrate

M->>M: db에 의존하는 서비스 생성

핵심 사항: Worker 초기화 중 SQLITE_BUSY 오류를 방지하기 위해, 다른 서비스를 생성하기 전에 반드시 await db.ready()가 완료되어야 합니다.

데이터베이스 최적화

DatabaseOptimizer 서비스는 주기적으로 유지보수 작업을 수행합니다:

작업설명빈도
PRAGMA optimize통계 업데이트앱 종료 시
VACUUM공간 회수수동 트리거
ANALYZE쿼리 플랜 업데이트주기적
WAL checkpointWAL을 메인 파일로 병합SQLite 자동

관련 파일

파일설명
packages/desktop/app/main/db/index.ts데이터베이스 초기화 (WAL + Drizzle + migrate)
packages/desktop/app/main/db/schema/Drizzle ORM 스키마 정의 디렉터리
packages/desktop/app/main/workers/DbClient.ts데이터베이스 Worker 클라이언트
packages/desktop/app/main/workers/db.worker.ts데이터베이스 Worker 구현
packages/desktop/app/main/workers/db/도메인별 분리된 DB 작업 모듈
packages/desktop/app/main/workers/types.tsDbRpc 인터페이스 정의
packages/desktop/app/main/services/persistence/db-optimizer/DatabaseOptimizer.ts데이터베이스 최적화 서비스
packages/desktop/app/main/services/platform/migration/MigrationService.ts커스텀 마이그레이션 서비스