Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
15 KiB
Trilium Notes - AI Coding Agent Instructions
Project Overview
Trilium Notes is a hierarchical note-taking application with advanced features like synchronization, scripting, and rich text editing. Built as a TypeScript monorepo using pnpm, it implements a three-layer caching architecture (Becca/Froca/Shaca) with a widget-based UI system and supports extensive user scripting capabilities.
Essential Architecture Patterns
Three-Layer Cache System (Critical to Understand)
- Becca (
apps/server/src/becca/): Server-side entity cache, primary data source - Froca (
apps/client/src/services/froca.ts): Client-side mirror synchronized via WebSocket - Shaca (
apps/server/src/share/): Optimized cache for public/shared notes
Key insight: Never bypass these caches with direct DB queries. Always use becca.notes[noteId], froca.getNote(), or equivalent cache methods.
Entity Relationship Model
Notes use a multi-parent tree via branches:
BNote- The note content and metadataBBranch- Tree relationships (one note can have multiple parents via cloning)BAttribute- Key-value metadata attached to notes (labels and relations)
Entity Change System & Sync
Every entity modification (notes, branches, attributes) creates an EntityChange record that drives synchronization:
// Entity changes are automatically tracked
note.title = "New Title";
note.save(); // Creates EntityChange record with changeId
// Sync protocol via WebSocket
ws.sendMessage({ type: 'sync-pull-in-progress', ... });
Critical: This is why you must use Becca/Froca methods instead of direct DB writes - they create the change tracking records needed for sync.
Entity Lifecycle & Events
The event system (apps/server/src/services/events.ts) broadcasts entity lifecycle events:
// Subscribe to events in widgets or services
eventService.subscribe('noteChanged', ({ noteId }) => {
// React to note changes
});
// Common events: noteChanged, branchChanged, attributeChanged, noteDeleted
// Widget method: entitiesReloadedEvent({loadResults}) for handling reloads
Becca loader priorities: Events are emitted in order (notes → branches → attributes) during initial load to ensure referential integrity.
TaskContext for Long Operations
Use TaskContext for operations with progress reporting (imports, exports, bulk operations):
const taskContext = new TaskContext("task-id", "import", "Import Notes");
taskContext.increaseProgressCount();
// WebSocket messages: { type: 'taskProgressCount', taskId, taskType, data, progressCount }
**Pattern**: All long-running operations (delete note trees, export, import) use TaskContext to send WebSocket updates to the frontend.
### Protected Session Handling
Protected notes require an active encryption session:
```typescript
// Always check before accessing protected content
if (note.isContentAvailable()) {
const content = note.getContent(); // Safe
} else {
const title = note.getTitleOrProtected(); // Returns "[protected]"
}
// Protected session management
protectedSessionService.isProtectedSessionAvailable() // Check session
protectedSessionService.startProtectedSession() // After password entry
Session timeout: Protected sessions expire after inactivity. The encryption key is kept in memory only.
Attribute Inheritance Patterns
Attributes can be inherited through three mechanisms:
// 1. Standard inheritance (#hidePromotedAttributes ~hidePromotedAttributes)
note.getInheritableAttributes() // Walks up parent tree
// 2. Child prefix inheritance (child:label copies to children)
parentNote.setLabel("child:icon", "book") // All children inherit this
// 3. Template relation inheritance (#template=templateNoteId)
note.setRelation("template", templateNoteId)
note.getInheritedAttributes() // Includes template's inheritable attributes
Cycle prevention: Inheritance tracking prevents infinite loops when notes reference each other.
Widget-Based UI Architecture
All UI components extend from widget base classes (apps/client/src/widgets/):
// Right panel widget (sidebar)
class MyWidget extends RightPanelWidget {
get position() { return 100; } // Order in panel
get parentWidget() { return 'right-pane'; }
isEnabled() { return this.note && this.note.hasLabel('myLabel'); }
async refreshWithNote(note) { /* Update UI */ }
}
// Note-aware widget (responds to note changes)
class MyNoteWidget extends NoteContextAwareWidget {
async refreshWithNote(note) { /* Refresh when note changes */ }
async entitiesReloadedEvent({loadResults}) { /* Handle entity updates */ }
}
Important: Widgets use jQuery (this.$widget) for DOM manipulation. Don't mix React patterns here.
Development Workflow
Running & Testing
# From root directory
pnpm install # Install dependencies
corepack enable # Enable pnpm if not available
pnpm server:start # Dev server (http://localhost:8080)
pnpm server:start-prod # Production mode server
pnpm desktop:start # Desktop app development
pnpm server:test spec/etapi/search.spec.ts # Run specific test
pnpm test:parallel # Client tests (can run parallel)
pnpm test:sequential # Server tests (sequential due to shared DB)
pnpm test:all # All tests (parallel + sequential)
pnpm coverage # Generate coverage reports
pnpm typecheck # Type check all projects
Building
pnpm client:build # Build client application
pnpm server:build # Build server application
pnpm desktop:build # Build desktop application
Test Organization
- Server tests (
apps/server/spec/): Must run sequentially (shared database state) - Client tests (
apps/client/src/): Can run in parallel - E2E tests (
apps/server-e2e/): Use Playwright for integration testing - ETAPI tests (
apps/server/spec/etapi/): External API contract tests
Pattern: When adding new API endpoints, add tests in spec/etapi/ following existing patterns (see search.spec.ts).
Monorepo Navigation
apps/
client/ # Frontend (shared by server & desktop)
server/ # Node.js backend with REST API
desktop/ # Electron wrapper
web-clipper/ # Browser extension for saving web content
db-compare/ # Database comparison tool
dump-db/ # Database export utility
edit-docs/ # Documentation editing tools
packages/
commons/ # Shared types and utilities
ckeditor5/ # Custom rich text editor with Trilium-specific plugins
codemirror/ # Code editor integration
highlightjs/ # Syntax highlighting
share-theme/ # Theme for shared/published notes
ckeditor5-admonition/ # Admonition blocks plugin
ckeditor5-footnotes/ # Footnotes plugin
ckeditor5-math/ # Math equations plugin
ckeditor5-mermaid/ # Mermaid diagrams plugin
Filter commands: Use pnpm --filter server test to run commands in specific packages.
Critical Code Patterns
ETAPI Backwards Compatibility
When adding query parameters to ETAPI endpoints (apps/server/src/etapi/), maintain backwards compatibility by checking if new params exist before changing response format.
Pattern: ETAPI consumers expect specific response shapes. Always check for breaking changes.
Frontend-Backend Communication
- REST API:
apps/server/src/routes/api/- Internal endpoints (no auth required whennoAuthentication=true) - ETAPI:
apps/server/src/etapi/- External API with authentication - WebSocket: Real-time sync via
apps/server/src/services/ws.ts
Auth note: ETAPI uses basic auth with tokens. Internal API endpoints trust the frontend.
Database Migrations
- Add scripts in
apps/server/src/migrations/YYMMDD_HHMM__description.sql - Update schema in
apps/server/src/assets/db/schema.sql - Never bypass Becca cache after migrations
Common Pitfalls
-
Never bypass the cache layers - Always use
becca.notes[noteId],froca.getNote(), or equivalent cache methods. Direct database queries will cause sync issues between Becca/Froca/Shaca and won't create EntityChange records needed for synchronization. -
Protected notes require session check - Before accessing
note.titleornote.getContent()on protected notes, checknote.isContentAvailable()or usenote.getTitleOrProtected()which handles this automatically. -
Widget lifecycle matters - Override
refreshWithNote()for note changes,doRenderBody()for initial render,entitiesReloadedEvent()for entity updates. Widgets use jQuery (this.$widget) - don't mix React patterns. -
Tests run differently - Server tests must run sequentially (shared database state), client tests can run in parallel. Use
pnpm test:sequentialfor backend,pnpm test:parallelfor frontend. -
ETAPI requires authentication - ETAPI endpoints use basic auth with tokens. Internal API endpoints (
apps/server/src/routes/api/) trust the frontend whennoAuthentication=true. -
Search expressions are evaluated in memory - The search service loads all matching notes, scores them in JavaScript, then sorts. You cannot add SQL-level LIMIT/OFFSET without losing scoring functionality.
-
Documentation edits have rules -
docs/Script API/is auto-generated (never edit directly).docs/User Guide/should be edited viapnpm edit-docs:edit-docs, not manually. Onlydocs/Developer Guide/anddocs/Release Notes/are safe for direct Markdown editing. -
pnpm workspace filtering - Use
pnpm --filter server <command>or shorthandpnpm server:testdefined in rootpackage.json. Note the--filtersyntax, not-For other shortcuts. -
Event subscription cleanup - When subscribing to events in widgets, unsubscribe in
cleanup()ordoDestroy()to prevent memory leaks. -
Attribute inheritance can be complex - When checking for labels/relations, use
note.getOwnedAttribute()for direct attributes ornote.getAttribute()for inherited ones. Don't assume attributes are directly on the note.
TypeScript Configuration
- Project references: Monorepo uses TypeScript project references (
tsconfig.json) - Path mapping: Use relative imports, not path aliases
- Build order:
pnpm typecheckbuilds all projects in dependency order - Build system: Uses Vite for fast development, ESBuild for production optimization
- Patches: Custom patches in
patches/directory for CKEditor and other dependencies
Key Files for Context
apps/server/src/becca/entities/bnote.ts- Note entity methodsapps/client/src/services/froca.ts- Frontend cache APIapps/server/src/services/search/services/search.ts- Search implementationapps/server/src/routes/routes.ts- API route registrationapps/client/src/widgets/basic_widget.ts- Widget base classapps/server/src/main.ts- Server startup entry pointapps/client/src/desktop.ts- Client initializationapps/server/src/services/backend_script_api.ts- Scripting APIapps/server/src/assets/db/schema.sql- Database schema
Note Types and Features
Trilium supports multiple note types with specialized widgets in apps/client/src/widgets/type_widgets/:
- Text: Rich text with CKEditor5 (markdown import/export)
- Code: Syntax-highlighted code editing with CodeMirror
- File: Binary file attachments
- Image: Image display with editing capabilities
- Canvas: Drawing/diagramming with Excalidraw
- Mermaid: Diagram generation
- Relation Map: Visual note relationship mapping
- Web View: Embedded web pages
- Doc/Book: Hierarchical documentation structure
Collections
Notes can be marked with the #collection label to enable collection view modes. Collections support multiple view types:
- List: Standard list view
- Grid: Card/grid layout
- Calendar: Calendar-based view
- Table: Tabular data view
- GeoMap: Geographic map view
- Board: Kanban-style board
- Presentation: Slideshow presentation mode
View types are configured via #viewType label (e.g., #viewType=table). Each view mode stores its configuration in a separate attachment (e.g., table.json). Collections are organized separately from regular note type templates in the note creation menu.
Common Development Tasks
Adding New Note Types
- Create widget in
apps/client/src/widgets/type_widgets/ - Register in
apps/client/src/services/note_types.ts - Add backend handling in
apps/server/src/services/notes.ts
Extending Search
- Search expressions handled in
apps/server/src/services/search/ - Add new search operators in search context files
- Remember: scoring happens in-memory, not at database level
Custom CKEditor Plugins
- Create new package in
packages/following existing plugin structure - Register in
packages/ckeditor5/src/plugins.ts - See
ckeditor5-admonition,ckeditor5-footnotes,ckeditor5-math,ckeditor5-mermaidfor examples
Database Migrations
- Add migration scripts in
apps/server/src/migrations/YYMMDD_HHMM__description.sql - Update schema in
apps/server/src/assets/db/schema.sql - Never bypass Becca cache after migrations
Security & Features
Security Considerations
- Per-note encryption with granular protected sessions
- CSRF protection for API endpoints
- OpenID and TOTP authentication support
- Sanitization of user-generated content
Scripting System
Trilium provides powerful user scripting capabilities:
- Frontend scripts: Run in browser context with UI access
- Backend scripts: Run in Node.js context with full API access
- Script API documentation in
docs/Script API/ - Backend API available via
apiobject in script context
Internationalization
- Translation files in
apps/client/src/translations/ - Use translation system via
t()function - Automatic pluralization: Add
_othersuffix to translation keys (e.g.,itemanditem_otherfor singular/plural)
Testing Conventions
// ETAPI test pattern
describe("etapi/feature", () => {
beforeAll(async () => {
config.General.noAuthentication = false;
app = await buildApp();
token = await login(app);
});
it("should test feature", async () => {
const response = await supertest(app)
.get("/etapi/notes?search=test")
.auth(USER, token, { type: "basic" })
.expect(200);
expect(response.body.results).toBeDefined();
});
});
Questions to Verify Understanding
Before implementing significant changes, confirm:
- Is this touching the cache layer? (Becca/Froca/Shaca must stay in sync via EntityChange records)
- Does this change API response shape? (Check backwards compatibility for ETAPI)
- Are you adding search features? (Understand expression-based architecture and in-memory scoring first)
- Is this a new widget? (Know which base class and lifecycle methods to use)
- Does this involve protected notes? (Check
isContentAvailable()before accessing content) - Is this a long-running operation? (Use TaskContext for progress reporting)
- Are you working with attributes? (Understand inheritance patterns: direct, child-prefix, template)