데이터베이스
Elftia는 better-sqlite3 + Drizzle ORM을 로컬 데이터베이스 솔루션으로 사용합니다. 모든 데이터베이스 작업은 Worker Thread를 통해 비동기적으로 실행됩니다.
기술 선택
| 항목 | 선택 | 이유 |
|---|---|---|
| 데이터베이스 엔진 | better-sqlite3 | 임베디드, 무설정, 고성능 |
| ORM | Drizzle 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_books | World Info 책 | Worker db/worldInfo.ts |
world_info_entries | World Info 항목 | Worker db/worldInfo.ts |
custom_agents | 커스텀 Agent | Worker db/customAgents.ts |
personas | Persona 정의 | Worker db/personas.ts |
user_skills | 사용자 Skill | Worker 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 checkpoint | WAL을 메인 파일로 병합 | 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.ts | DbRpc 인터페이스 정의 |
packages/desktop/app/main/services/persistence/db-optimizer/DatabaseOptimizer.ts | 데이터베이스 최적화 서비스 |
packages/desktop/app/main/services/platform/migration/MigrationService.ts | 커스텀 마이그레이션 서비스 |