30 KiB
Vendored
Trilium Notes - Technical Architecture Documentation
Version: 0.99.3
Last Updated: November 2025
Maintainer: TriliumNext Team
Table of Contents
- Introduction
- High-Level Architecture
- Monorepo Structure
- Core Architecture Patterns
- Data Layer
- Caching System
- Frontend Architecture
- Backend Architecture
- API Architecture
- Build System
- Testing Strategy
- Security Architecture
- Related Documentation
Introduction
Trilium Notes is a hierarchical note-taking application built as a TypeScript monorepo. It supports multiple deployment modes (desktop, server, mobile web) and features advanced capabilities including synchronization, scripting, encryption, and rich content editing.
Key Characteristics
- Monorepo Architecture: Uses pnpm workspaces for dependency management
- Multi-Platform: Desktop (Electron), Server (Node.js/Express), and Mobile Web
- TypeScript-First: Strong typing throughout the codebase
- Plugin-Based: Extensible architecture for note types and UI components
- Offline-First: Full functionality without network connectivity
- Synchronization-Ready: Built-in sync protocol for multi-device usage
Technology Stack
- Runtime: Node.js (backend), Browser/Electron (frontend)
- Language: TypeScript, JavaScript
- Database: SQLite (better-sqlite3)
- Build Tools: Vite, ESBuild, pnpm
- UI Framework: Custom widget-based system
- Rich Text: CKEditor 5 (customized)
- Code Editing: CodeMirror 6
- Desktop: Electron
- Server: Express.js
High-Level Architecture
Trilium follows a client-server architecture even in desktop mode, where Electron runs both the backend server and frontend client within the same process.
┌─────────────────────────────────────────────────────────────┐
│ Frontend │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Widgets │ │ Froca │ │ UI │ │
│ │ System │ │ Cache │ │ Services │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │ │
│ WebSocket / REST API │
│ │ │
└─────────────────────────┼────────────────────────────────────┘
│
┌─────────────────────────┼────────────────────────────────────┐
│ Backend Server │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ Express │ │ Becca │ │ Script │ │
│ │ Routes │ │ Cache │ │ Engine │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │ │
│ ┌────┴─────┐ │
│ │ SQLite │ │
│ │ Database │ │
│ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
Deployment Modes
-
Desktop Application
- Electron wrapper running both frontend and backend
- Local SQLite database
- Full offline functionality
- Cross-platform (Windows, macOS, Linux)
-
Server Installation
- Node.js server exposing web interface
- Multi-user capable
- Can sync with desktop clients
- Docker deployment supported
-
Mobile Web
- Optimized responsive interface
- Accessed via browser
- Requires server installation
Monorepo Structure
Trilium uses pnpm workspaces to manage its monorepo structure, with apps and packages clearly separated.
trilium/
├── apps/ # Runnable applications
│ ├── client/ # Frontend application (shared by server & desktop)
│ ├── server/ # Node.js server with web interface
│ ├── desktop/ # Electron desktop application
│ ├── web-clipper/ # Browser extension for web content capture
│ ├── db-compare/ # Database comparison tool
│ ├── dump-db/ # Database export tool
│ ├── edit-docs/ # Documentation editing tool
│ ├── build-docs/ # Documentation build tool
│ └── website/ # Marketing website
│
├── packages/ # Shared libraries
│ ├── commons/ # Shared interfaces and utilities
│ ├── ckeditor5/ # Custom rich text editor
│ ├── codemirror/ # Code editor customizations
│ ├── highlightjs/ # Syntax highlighting
│ ├── ckeditor5-admonition/ # CKEditor plugin: admonitions
│ ├── ckeditor5-footnotes/ # CKEditor plugin: footnotes
│ ├── ckeditor5-keyboard-marker/# CKEditor plugin: keyboard shortcuts
│ ├── ckeditor5-math/ # CKEditor plugin: math equations
│ ├── ckeditor5-mermaid/ # CKEditor plugin: diagrams
│ ├── express-partial-content/ # HTTP partial content middleware
│ ├── share-theme/ # Shared note theme
│ ├── splitjs/ # Split pane library
│ └── turndown-plugin-gfm/ # Markdown conversion
│
├── docs/ # Documentation
├── scripts/ # Build and utility scripts
└── patches/ # Package patches (via pnpm)
Package Dependencies
The monorepo uses workspace protocol (workspace:*) for internal dependencies:
desktop → client → commons
server → client → commons
client → ckeditor5, codemirror, highlightjs
ckeditor5 → ckeditor5-* plugins
Core Architecture Patterns
Three-Layer Cache System
Trilium implements a sophisticated three-tier caching system to optimize performance and enable offline functionality:
1. Becca (Backend Cache)
Located at: apps/server/src/becca/
// Becca caches all entities in memory
class Becca {
notes: Record<string, BNote>
branches: Record<string, BBranch>
attributes: Record<string, BAttribute>
attachments: Record<string, BAttachment>
// ... other entity collections
}
Responsibilities:
- Server-side entity cache
- Maintains complete note tree in memory
- Handles entity relationships and integrity
- Provides fast lookups without database queries
- Manages entity lifecycle (create, update, delete)
Key Files:
becca.ts- Main cache instancebecca_loader.ts- Loads entities from databasebecca_service.ts- Cache management operationsentities/- Entity classes (BNote, BBranch, etc.)
2. Froca (Frontend Cache)
Located at: apps/client/src/services/froca.ts
// Froca is a read-only mirror of backend data
class Froca {
notes: Record<string, FNote>
branches: Record<string, FBranch>
attributes: Record<string, FAttribute>
// ... other entity collections
}
Responsibilities:
- Frontend read-only cache
- Lazy loading of note tree
- Minimizes API calls
- Enables fast UI rendering
- Synchronizes with backend via WebSocket
Loading Strategy:
- Initial load: root notes and immediate children
- Lazy load: notes loaded when accessed
- When note is loaded, all parent and child branches load
- Deleted entities tracked via missing branches
3. Shaca (Share Cache)
Located at: apps/server/src/share/
Responsibilities:
- Optimized cache for shared/published notes
- Handles public note access without authentication
- Performance-optimized for high-traffic scenarios
- Separate from main Becca to isolate concerns
Entity System
Trilium's data model is based on five core entities:
┌──────────────────────────────────────────────────────────┐
│ Note Tree │
│ │
│ ┌─────────┐ │
│ │ Note │ │
│ │ (BNote) │ │
│ └────┬────┘ │
│ │ │
│ │ linked by │
│ ▼ │
│ ┌──────────┐ ┌─────────────┐ │
│ │ Branch │◄────────│ Attribute │ │
│ │(BBranch) │ │ (BAttribute)│ │
│ └──────────┘ └─────────────┘ │
│ │ │
│ │ creates │
│ ▼ │
│ ┌──────────┐ ┌─────────────┐ │
│ │ Revision │ │ Attachment │ │
│ │(BRevision│ │(BAttachment)│ │
│ └──────────┘ └─────────────┘ │
│ │
└──────────────────────────────────────────────────────────┘
Entity Definitions
1. BNote (apps/server/src/becca/entities/bnote.ts)
- Represents a note with title, content, and metadata
- Type can be: text, code, file, image, canvas, mermaid, etc.
- Contains content via blob reference
- Can be protected (encrypted)
- Has creation and modification timestamps
2. BBranch (apps/server/src/becca/entities/bbranch.ts)
- Represents parent-child relationship between notes
- Enables note cloning (multiple parents)
- Contains positioning information
- Has optional prefix for customization
- Tracks expansion state in tree
3. BAttribute (apps/server/src/becca/entities/battribute.ts)
- Key-value metadata attached to notes
- Two types: labels (tags) and relations (links)
- Can be inheritable to child notes
- Used for search, organization, and scripting
- Supports promoted attributes (displayed prominently)
4. BRevision (apps/server/src/becca/entities/brevision.ts)
- Stores historical versions of note content
- Automatic versioning on edits
- Retains title, type, and content
- Enables note history browsing and restoration
5. BAttachment (apps/server/src/becca/entities/battachment.ts)
- File attachments linked to notes
- Has owner (note), role, and mime type
- Content stored in blobs
- Can be protected (encrypted)
6. BBlob (apps/server/src/becca/entities/bblob.ts)
- Binary large object storage
- Stores actual note content and attachments
- Referenced by notes, revisions, and attachments
- Supports encryption for protected content
Widget-Based UI
The frontend uses a widget system for modular, reusable UI components.
Located at: apps/client/src/widgets/
// Widget Hierarchy
BasicWidget
├── NoteContextAwareWidget (responds to note changes)
│ ├── RightPanelWidget (displayed in right sidebar)
│ └── Type-specific widgets
├── Container widgets (tabs, ribbons, etc.)
└── Specialized widgets (search, calendar, etc.)
Base Classes:
-
BasicWidget (
basic_widget.ts)- Base class for all UI components
- Lifecycle: construction → rendering → events → destruction
- Handles DOM manipulation
- Event subscription management
- Child widget management
-
NoteContextAwareWidget (
note_context_aware_widget.ts)- Extends BasicWidget
- Automatically updates when active note changes
- Accesses current note context
- Used for note-dependent UI
-
RightPanelWidget
- Widgets displayed in right sidebar
- Collapsible sections
- Context-specific tools and information
Type-Specific Widgets:
Located at: apps/client/src/widgets/type_widgets/
Each note type has a dedicated widget:
text_type_widget.ts- CKEditor integrationcode_type_widget.ts- CodeMirror integrationfile_type_widget.ts- File preview and downloadimage_type_widget.ts- Image display and editingcanvas_type_widget.ts- Excalidraw integrationmermaid_type_widget.ts- Diagram rendering- And more...
Data Layer
Database Schema
Trilium uses SQLite as its database engine, managed via better-sqlite3.
Schema location: apps/server/src/assets/db/schema.sql
Core Tables:
-- Notes: Core content storage
notes (
noteId, title, isProtected, type, mime,
blobId, isDeleted, dateCreated, dateModified
)
-- Branches: Tree relationships
branches (
branchId, noteId, parentNoteId, notePosition,
prefix, isExpanded, isDeleted
)
-- Attributes: Metadata
attributes (
attributeId, noteId, type, name, value,
position, isInheritable, isDeleted
)
-- Revisions: Version history
revisions (
revisionId, noteId, type, mime, title,
blobId, utcDateLastEdited
)
-- Attachments: File attachments
attachments (
attachmentId, ownerId, role, mime, title,
blobId, isProtected, isDeleted
)
-- Blobs: Binary content
blobs (
blobId, content, dateModified
)
-- Options: Application settings
options (
name, value, isSynced
)
-- Entity Changes: Sync tracking
entity_changes (
entityName, entityId, hash, changeId,
isSynced, utcDateChanged
)
Data Access Patterns
Direct SQL:
// apps/server/src/services/sql.ts
sql.getRows("SELECT * FROM notes WHERE type = ?", ['text'])
sql.execute("UPDATE notes SET title = ? WHERE noteId = ?", [title, noteId])
Through Becca:
// Recommended approach - uses cache
const note = becca.getNote('noteId')
note.title = 'New Title'
note.save()
Through Froca (Frontend):
// Read-only access
const note = froca.getNote('noteId')
console.log(note.title)
Database Migrations
Migration system: apps/server/src/migrations/
- Sequential numbered files (e.g.,
XXXX_migration_name.sql) - Automatic execution on version upgrade
- Schema version tracked in options table
- Both SQL and JavaScript migrations supported
Caching System
Cache Initialization
Backend (Becca):
// On server startup
await becca_loader.load() // Loads all entities into memory
becca.loaded = true
Frontend (Froca):
// On app initialization
await froca.loadInitialTree() // Loads root and visible notes
// Lazy load on demand
const note = await froca.getNote(noteId) // Triggers load if not cached
Cache Invalidation
Server-Side:
- Entities automatically update cache on save
- WebSocket broadcasts changes to all clients
- Synchronization updates trigger cache refresh
Client-Side:
- WebSocket listeners update Froca
- Manual reload via
froca.loadSubTree(noteId) - Full reload on protected session changes
Cache Consistency
Entity Change Tracking:
// Every entity modification tracked
entity_changes (
entityName: 'notes',
entityId: 'note123',
hash: 'abc...',
changeId: 'change456',
utcDateChanged: '2025-11-02...'
)
Sync Protocol:
- Client requests changes since last sync
- Server returns entity_changes records
- Client applies changes to Froca
- Client sends local changes to server
- Server updates Becca and database
Frontend Architecture
Application Entry Point
Desktop: apps/client/src/desktop.ts
Web: apps/client/src/index.ts
Service Layer
Located at: apps/client/src/services/
Key services:
froca.ts- Frontend cacheserver.ts- API communicationws.ts- WebSocket connectiontree_service.ts- Note tree managementnote_context.ts- Active note trackingprotected_session.ts- Encryption key managementlink.ts- Note linking and navigationexport.ts- Note export functionality
UI Components
Main Layout:
┌──────────────────────────────────────────────────────┐
│ Title Bar │
├──────────┬────────────────────────┬──────────────────┤
│ │ │ │
│ Note │ Note Detail │ Right Panel │
│ Tree │ Editor │ (Info, Links) │
│ │ │ │
│ │ │ │
├──────────┴────────────────────────┴──────────────────┤
│ Status Bar │
└──────────────────────────────────────────────────────┘
Component Locations:
widgets/containers/- Layout containerswidgets/buttons/- Toolbar buttonswidgets/dialogs/- Modal dialogswidgets/ribbon_widgets/- Tab widgetswidgets/type_widgets/- Note type editors
Event System
Application Events:
// Subscribe to events
appContext.addBeforeUnloadListener(() => {
// Cleanup before page unload
})
// Trigger events
appContext.trigger('noteTreeLoaded')
Note Context Events:
// NoteContextAwareWidget automatically receives:
- noteSwitched()
- noteChanged()
- refresh()
State Management
Trilium uses custom state management rather than Redux/MobX:
note_context.ts- Active note and contextfroca.ts- Entity cache- Component local state
- URL parameters for shareable state
Backend Architecture
Application Entry Point
Location: apps/server/src/main.ts
Startup Sequence:
- Load configuration
- Initialize database
- Run migrations
- Load Becca cache
- Start Express server
- Initialize WebSocket
- Start scheduled tasks
Service Layer
Located at: apps/server/src/services/
Core Services:
-
Notes Management
notes.ts- CRUD operationsnote_contents.ts- Content handlingnote_types.ts- Type-specific logiccloning.ts- Note cloning/multi-parent
-
Tree Operations
tree.ts- Tree structure managementbranches.ts- Branch operationsconsistency_checks.ts- Tree integrity
-
Search
search/search.ts- Main search enginesearch/expressions/- Search expression parsingsearch/services/- Search utilities
-
Sync
sync.ts- Synchronization protocolsync_update.ts- Update handlingsync_mutex.ts- Concurrency control
-
Scripting
backend_script_api.ts- Backend script APIscript_context.ts- Script execution context
-
Import/Export
import/- Various import formatsexport/- Export to different formatszip.ts- Archive handling
-
Security
encryption.ts- Note encryptionprotected_session.ts- Session managementpassword.ts- Password handling
Route Structure
Located at: apps/server/src/routes/
routes/
├── index.ts # Route registration
├── api/ # REST API endpoints
│ ├── notes.ts
│ ├── branches.ts
│ ├── attributes.ts
│ ├── search.ts
│ ├── login.ts
│ └── ...
└── custom/ # Special endpoints
├── setup.ts
├── share.ts
└── ...
API Endpoint Pattern:
router.get('/api/notes/:noteId', (req, res) => {
const noteId = req.params.noteId
const note = becca.getNote(noteId)
res.json(note.getPojoWithContent())
})
Middleware
Key middleware components:
auth.ts- Authenticationcsrf.ts- CSRF protectionrequest_context.ts- Request-scoped dataerror_handling.ts- Error responses
API Architecture
Internal API
REST Endpoints (/api/*)
Used by the frontend for all operations:
Note Operations:
GET /api/notes/:noteId- Get notePOST /api/notes/:noteId/content- Update contentPUT /api/notes/:noteId- Update metadataDELETE /api/notes/:noteId- Delete note
Tree Operations:
GET /api/tree- Get note treePOST /api/branches- Create branchPUT /api/branches/:branchId- Update branchDELETE /api/branches/:branchId- Delete branch
Search:
GET /api/search?query=...- Search notesGET /api/search-note/:noteId- Execute search note
ETAPI (External API)
Located at: apps/server/src/etapi/
Purpose: Third-party integrations and automation
Authentication: Token-based (ETAPI tokens)
OpenAPI Spec: Auto-generated
Key Endpoints:
/etapi/notes- Note CRUD/etapi/branches- Branch management/etapi/attributes- Attribute operations/etapi/attachments- Attachment handling
Example:
curl -H "Authorization: YOUR_TOKEN" \
https://trilium.example.com/etapi/notes/noteId
WebSocket API
Located at: apps/server/src/services/ws.ts
Purpose: Real-time updates and synchronization
Protocol: WebSocket (Socket.IO-like custom protocol)
Message Types:
sync- Synchronization requestentity-change- Entity update notificationrefresh-tree- Tree structure changedopen-note- Open note in UI
Client Subscribe:
ws.subscribe('entity-change', (data) => {
froca.processEntityChange(data)
})
Build System
Package Manager: pnpm
Why pnpm:
- Fast, disk-efficient
- Strict dependency isolation
- Native monorepo support via workspaces
- Patch package support
Workspace Configuration:
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
Build Tools
Vite (Development & Production)
- Fast HMR for development
- Optimized production builds
- Asset handling
- Plugin ecosystem
ESBuild (TypeScript compilation)
- Fast TypeScript transpilation
- Bundling support
- Minification
TypeScript
- Project references for monorepo
- Strict type checking
- Shared
tsconfig.base.json
Build Scripts
Root package.json scripts:
{
"server:start": "pnpm run --filter server dev",
"server:build": "pnpm run --filter server build",
"client:build": "pnpm run --filter client build",
"desktop:build": "pnpm run --filter desktop build",
"test:all": "pnpm test:parallel && pnpm test:sequential"
}
Build Process
Development:
pnpm install # Install dependencies
pnpm server:start # Start dev server (port 8080)
# or
pnpm desktop:start # Start Electron dev
Production (Server):
pnpm server:build # Build server + client
node apps/server/dist/main.js
Production (Desktop):
pnpm desktop:build # Build Electron app
# Creates distributable in apps/desktop/out/make/
Docker:
docker build -t trilium .
docker run -p 8080:8080 trilium
Asset Pipeline
Client Assets:
- Entry:
apps/client/src/index.html - Bundled by Vite
- Output:
apps/client/dist/
Server Static:
- Serves client assets in production
- Public directory:
apps/server/public/
Desktop:
- Packages client assets
- Electron main process:
apps/desktop/src/main.ts - Electron renderer: loads client app
Testing Strategy
Test Organization
Parallel Tests (can run simultaneously):
- Client tests
- Package tests
- E2E tests (isolated databases)
Sequential Tests (shared resources):
- Server tests (shared database)
- CKEditor plugin tests
Test Frameworks
- Vitest - Unit and integration tests
- Playwright - E2E tests
- Happy-DOM - DOM testing environment
Running Tests
pnpm test:all # All tests
pnpm test:parallel # Fast parallel tests
pnpm test:sequential # Sequential tests only
pnpm coverage # With coverage reports
Test Locations
apps/
├── server/
│ └── src/**/*.spec.ts # Server tests
├── client/
│ └── src/**/*.spec.ts # Client tests
└── server-e2e/
└── tests/**/*.spec.ts # E2E tests
E2E Testing
Server E2E:
- Tests full REST API
- Tests WebSocket functionality
- Tests sync protocol
Desktop E2E:
- Playwright with Electron
- Tests full desktop app
- Screenshot comparison
Security Architecture
Encryption System
Per-Note Encryption:
- Notes can be individually protected
- AES-256 encryption
- Password-derived encryption key (PBKDF2)
- Separate protected session management
Protected Session:
- Time-limited access to protected notes
- Automatic timeout
- Re-authentication required
- Frontend:
protected_session.ts - Backend:
protected_session.ts
Authentication
Password Auth:
- PBKDF2 key derivation
- Salt per installation
- Hash verification
OpenID Connect:
- External identity provider support
- OAuth 2.0 flow
- Configurable providers
TOTP (2FA):
- Time-based one-time passwords
- QR code setup
- Backup codes
Authorization
Single-User Model:
- Desktop: single user (owner)
- Server: single user per installation
Share Notes:
- Public access without authentication
- Separate Shaca cache
- Read-only access
CSRF Protection
CSRF Tokens:
- Required for state-changing operations
- Token in header or cookie
- Validation middleware
Input Sanitization
XSS Prevention:
- DOMPurify for HTML sanitization
- CKEditor content filtering
- CSP headers
SQL Injection:
- Parameterized queries only
- Better-sqlite3 prepared statements
- No string concatenation in SQL
Dependency Security
Vulnerability Scanning:
- Renovate bot for updates
- npm audit integration
- Override vulnerable sub-dependencies
Related Documentation
User Documentation
- User Guide - End-user features and usage
- Installation Guide
- Basic Concepts
Developer Documentation
- Developer Guide - Development setup
- Environment Setup
- Project Structure
- Adding Note Types
- Database Schema
API Documentation
- Script API - User scripting API
- ETAPI Documentation - External API
Additional Resources
- CLAUDE.md - AI assistant guidance
- README.md - Project overview
- SECURITY.md - Security policy
Appendices
Glossary
- Becca: Backend Cache - server-side entity cache
- Froca: Frontend Cache - client-side entity mirror
- Shaca: Share Cache - cache for public shared notes
- ETAPI: External API for third-party integrations
- Protected Note: Encrypted note requiring password
- Clone: Note with multiple parent branches
- Branch: Parent-child relationship between notes
- Attribute: Metadata (label or relation) attached to note
- Blob: Binary large object containing note content
File Naming Conventions
BEntity- Backend entity (e.g., BNote, BBranch)FEntity- Frontend entity (e.g., FNote, FBranch)*_widget.ts- Widget classes*_service.ts- Service modules*.spec.ts- Test filesXXXX_*.sql- Migration files
Architecture Decision Records
For historical context on major architectural decisions, see:
- Migration to TypeScript monorepo
- Adoption of pnpm workspaces
- CKEditor 5 upgrade
- Entity change tracking system
Document Maintainer: TriliumNext Team
Last Review: November 2025
Next Review: When major architectural changes occur