16 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Overview
Trilium Notes is a hierarchical note-taking application with synchronization, scripting, and rich text editing. TypeScript monorepo using pnpm with multiple apps and shared packages.
Development Commands
# Setup
corepack enable && pnpm install
# Run
pnpm server:start # Dev server at http://localhost:8080
pnpm desktop:start # Electron dev app
pnpm standalone:start # Standalone client dev
# Build
pnpm client:build # Frontend
pnpm server:build # Backend
pnpm desktop:build # Electron
# Test
pnpm test:all # All tests (parallel + sequential)
pnpm test:parallel # Client + most package tests
pnpm test:sequential # Server, ckeditor5-mermaid, ckeditor5-math (shared DB)
pnpm --filter server test # Single package tests
pnpm coverage # Coverage reports
# Lint & Format
pnpm dev:linter-check # ESLint check
pnpm dev:linter-fix # ESLint fix
pnpm dev:format-check # Format check (stricter stylistic rules)
pnpm dev:format-fix # Format fix
pnpm typecheck # TypeScript type check across all projects
Running a single test file: pnpm --filter server test spec/etapi/search.spec.ts
Main Applications
The four main apps share packages/trilium-core/ for business logic but differ in runtime:
- client (
apps/client/): Preact frontend with jQuery widget system. Shared UI layer used by both server and desktop. - server (
apps/server/): Node.js backend (Express, better-sqlite3). Serves the client and provides REST/WebSocket APIs. - desktop (
apps/desktop/): Electron wrapper around server + client, running both in a single process. - standalone (
apps/client-standalone/+apps/standalone-desktop/): Runs the entire stack in the browser — server logic compiled to WASM via sql.js, executed in a service worker. No Node.js dependency at runtime.
Monorepo Structure
apps/
client/ # Preact frontend (shared by server, desktop, standalone)
server/ # Node.js backend (Express, better-sqlite3)
desktop/ # Electron (bundles server + client)
client-standalone/ # Standalone client (WASM + service workers, no Node.js)
standalone-desktop/ # Standalone desktop variant
server-e2e/ # Playwright E2E tests for server
web-clipper/ # Browser extension
website/ # Project website
db-compare/, dump-db/, edit-docs/, build-docs/, icon-pack-builder/
packages/
trilium-core/ # Core business logic: entities, services, SQL, sync
commons/ # Shared interfaces and utilities
ckeditor5/ # Custom rich text editor bundle
codemirror/ # Code editor integration
highlightjs/ # Syntax highlighting
share-theme/ # Theme for shared/published notes
ckeditor5-admonition/, ckeditor5-footnotes/, ckeditor5-math/, ckeditor5-mermaid/
ckeditor5-keyboard-marker/, express-partial-content/, pdfjs-viewer/, splitjs/
turndown-plugin-gfm/
Use pnpm --filter <package-name> <command> to run commands in specific packages.
Core Architecture
Three-Layer Cache System
All data access goes through cache layers — never bypass with direct DB queries:
- Becca (
packages/trilium-core/src/becca/): Server-side entity cache. Access viabecca.notes[noteId]. - Froca (
apps/client/src/services/froca.ts): Client-side mirror synced via WebSocket. Access viafroca.getNote(). - Shaca (
apps/server/src/share/): Optimized cache for shared/published notes.
Critical: Always use cache methods, not direct DB writes. Cache methods create EntityChange records needed for synchronization.
Entity System
Core entities live in packages/trilium-core/src/becca/entities/ (not apps/server/):
BNote— Notes with content and metadataBBranch— Multi-parent tree relationships (cloning supported)BAttribute— Key-value metadata (labels and relations)BRevision— Version historyBOption— Application configurationBBlob— Binary content storage
Entities extend AbstractBeccaEntity<T> with built-in change tracking, hash generation, and date management.
Entity Change & Sync Protocol
Every entity modification creates an EntityChange record driving sync:
- Login with HMAC authentication (document secret + timestamp)
- Push changes → Pull changes → Push again (conflict resolution)
- Content hash verification with retry loop
Sync services: packages/trilium-core/src/services/sync.ts, syncMutexService, syncUpdateService.
Widget-Based UI
Frontend widgets in apps/client/src/widgets/:
BasicWidget/TypedBasicWidget— Base classes (jQuerythis.$widgetfor DOM)NoteContextAwareWidget— Responds to note changesRightPanelWidget— Sidebar widgets with position ordering- Type-specific widgets in
type_widgets/directory
Widget lifecycle: doRenderBody() for initial render, refreshWithNote() for note changes, entitiesReloadedEvent({loadResults}) for entity updates. Uses jQuery — don't mix React patterns.
Fluent builder pattern: .child(), .class(), .css() chaining with position-based ordering.
API Architecture
- Internal API (
apps/server/src/routes/api/): REST endpoints, trusts frontend - ETAPI (
apps/server/src/etapi/): External API with basic auth tokens — maintain backwards compatibility - WebSocket (
apps/server/src/services/ws.ts): Real-time sync
Platform Abstraction
packages/trilium-core/src/services/platform.ts defines PlatformProvider interface with implementations in apps/desktop/, apps/server/, and apps/client-standalone/. Singleton via initPlatform()/getPlatform().
PlatformProvider provides:
crash(message)— Platform-specific fatal error handlinggetEnv(key)— Environment variable access (server/desktop useprocess.env, standalone maps URL query params like?safeMode→TRILIUM_SAFE_MODE)isElectron,isMac,isWindows— Platform detection flags
Critical rules for trilium-core:
- No
process.envin core — usegetPlatform().getEnv()instead (not available in standalone/browser) - No
import path from "path"in core — Node'spathmodule is externalized in browser builds. Usepackages/trilium-core/src/services/utils/path.tsforextname()/basename()equivalents - No Node.js built-in modules in core — core runs in both Node.js and the browser (standalone). Use platform-agnostic alternatives or platform providers
- Platform detection via functions —
isElectron(),isMac(),isWindows()fromutils/index.tsare functions (not constants) that callgetPlatform(). They can only be called afterinitializeCore(), not at module top-level. If used in static definitions, wrap in a closure:value: () => isWindows() ? "0.9" : "1.0" - Barrel import caution —
import { x } from "@triliumnext/core"loads ALL core exports. Early-loading modules likeconfig.tsshould import specific subpaths (e.g.@triliumnext/core/src/services/utils/index) to avoid circular dependencies or initialization ordering issues - Electron IPC — In desktop mode, client API calls use Electron IPC (not HTTP). The IPC handler in
apps/server/src/routes/electron.tsmust be registered viautils.isElectronfrom the server's utils (which correctly checksprocess.versions["electron"]), not from core's utils
Database
SQLite via better-sqlite3. SQL abstraction in packages/trilium-core/src/services/sql/ with DatabaseProvider interface, prepared statement caching, and transaction support.
- Schema:
apps/server/src/assets/db/schema.sql - Migrations:
apps/server/src/migrations/YYMMDD_HHMM__description.sql
Internationalization
- Translation files in
apps/client/src/translations/ - Supported languages: English, German, Spanish, French, Romanian, Chinese
- Only add new translation keys to
en/translation.json— translations for other languages are managed via Weblate and will be contributed by the community - Third-party components (e.g., mind-map context menu) should use i18next
t()for their labels, with the English strings added toen/translation.jsonunder a dedicated namespace (e.g.,"mind-map") - When a translated string contains interpolated components (e.g. links, note references) whose order may vary across languages, use
<Trans>fromreact-i18nextinstead oft(). This lets translators reorder components freely (e.g."<Note/> in <Parent/>"vs"in <Parent/>, <Note/>") - When adding a new locale, follow the step-by-step guide in
docs/Developer Guide/Developer Guide/Concepts/Internationalisation Translations/Adding a new locale.md - Server-side translations (e.g. hidden subtree titles) go in
apps/server/src/assets/translations/en/server.json, not in the clienttranslation.json
Electron Desktop App
- Desktop entry point:
apps/desktop/src/main.ts, window management:apps/server/src/services/window.ts - IPC communication: use
electron.ipcMain.on(channel, handler)on server side,electron.ipcRenderer.send(channel, data)on client side - Electron-only features should check
isElectron()fromapps/client/src/services/utils.ts(client) orutils.isElectron(server)
Three inheritance mechanisms:
- Standard:
note.getInheritableAttributes()walks parent tree - Child prefix:
child:labelon parent copies to children - Template relation:
#template=noteNoteIdincludes template's inheritable attributes
Attribute Inheritance
Use note.getOwnedAttribute() for direct, note.getAttribute() for inherited.
Client-Side API Restrictions
- Do not use
crypto.randomUUID()or other Web Crypto APIs that require secure contexts - Trilium can run over HTTP, not just HTTPS - Use
randomString()fromapps/client/src/services/utils.tsfor generating IDs instead
Shared Types Policy
- Types shared between client and server belong in
@triliumnext/commons(packages/commons/src/lib/) - Import shared types directly from
@triliumnext/commons- do not re-export them from app-specific modules - Keep app-specific types (e.g.,
LlmProviderfor server,StreamCallbacksfor client) in their respective apps
Important Patterns
- Protected notes: Check
note.isContentAvailable()before accessing content; usenote.getTitleOrProtected()for safe title access - Long operations: Use
TaskContextfor progress reporting via WebSocket - Event system (
packages/trilium-core/src/services/events.ts): Events emitted in order (notes → branches → attributes) during load for referential integrity - Search: Expression-based, scoring happens in-memory — cannot add SQL-level LIMIT/OFFSET without losing scoring
- Widget cleanup: Unsubscribe from events in
cleanup()/doDestroy()to prevent memory leaks
Code Style
- 4-space indentation, semicolons always required
- Double quotes (enforced by format config)
- Max line length: 100 characters
- Unix line endings
- Import sorting via
eslint-plugin-simple-import-sort
Testing
- Server tests (
apps/server/spec/): Vitest, must run sequentially (shared DB), forks pool, max 6 workers - Client tests (
apps/client/src/): Vitest with happy-dom environment, can run in parallel - E2E tests (
apps/server-e2e/): Playwright, Chromium, server started automatically on port 8082 - ETAPI tests (
apps/server/spec/etapi/): External API contract tests
Documentation
docs/Script API/— Auto-generated, never edit directlydocs/User Guide/— Edit viapnpm edit-docs:edit-docs, not manuallydocs/Developer Guide/anddocs/Release Notes/— Safe for direct Markdown editing
Key Entry Points
apps/server/src/main.ts— Server startupapps/client/src/desktop.ts— Client initializationpackages/trilium-core/src/becca/becca.ts— Backend data managementapps/client/src/services/froca.ts— Frontend cacheapps/server/src/routes/routes.ts— API route registrationpackages/trilium-core/src/services/sql/sql.ts— Database abstraction
Adding Hidden System Notes
The hidden subtree (_hidden) contains system notes with predictable IDs (prefixed with _). Defined in apps/server/src/services/hidden_subtree.ts via the HiddenSubtreeItem interface from @triliumnext/commons.
- Add the note definition to
buildHiddenSubtreeDefinition()inapps/server/src/services/hidden_subtree.ts - Add a translation key for the title in
apps/server/src/assets/translations/en/server.jsonunder"hidden-subtree" - The note is auto-created on startup by
checkHiddenSubtree()— uses deterministic IDs so all sync cluster instances generate the same structure - Key properties:
id(must start with_),title,type,icon(format:bx-icon-namewithoutbxprefix),attributes,children,content - Use
enforceAttributes: trueto keep attributes in sync,enforceBranches: truefor correct placement,enforceDeleted: trueto remove deprecated notes - For launcher bar entries, see
hidden_subtree_launcherbar.ts; for templates, seehidden_subtree_templates.ts
Writing to Notes from Server Services
note.setContent()requires a CLS (Continuation Local Storage) context — wrap calls incls.init(() => { ... })(fromapps/server/src/services/cls.ts)- Operations called from Express routes already have CLS context; standalone services (schedulers, Electron IPC handlers) do not
Adding New LLM Tools
Tools are defined using defineTools() in apps/server/src/services/llm/tools/ and automatically registered for both the LLM chat and MCP server.
- Add the tool definition in the appropriate module (
note_tools.ts,attribute_tools.ts,attachment_tools.ts,hierarchy_tools.ts) or create a new module - Each tool needs:
description,inputSchema(Zod),executefunction, and optionallymutates: truefor write operations - If creating a new module, wrap tools in
defineTools({...})and add the registry toallToolRegistriesintools/index.ts - Add a client-side friendly name in
apps/client/src/translations/en/translation.jsonunderllm.tools.<tool_name>— use imperative tense (e.g. "Search notes", "Create note", "Get attributes"), not present continuous - Use ETAPI (
apps/server/src/etapi/) as inspiration for what fields to expose, but do not import ETAPI mappers — inline the field mappings directly in the tool so the LLM layer stays decoupled from the API layer
Updating PDF.js
- Update
pdfjs-distversion inpackages/pdfjs-viewer/package.json - Run
npx tsx scripts/update-viewer.tsfrom that directory - Run
pnpm buildto verify success - Commit all changes including updated viewer files
Database Migrations
- Add migration scripts in
apps/server/src/migrations/ - Update schema in
apps/server/src/assets/db/schema.sql
Server-Side Static Assets
- Static assets (templates, SQL, translations, etc.) go in
apps/server/src/assets/ - Access them at runtime via
RESOURCE_DIRfromapps/server/src/services/resource_dir.ts(e.g.path.join(RESOURCE_DIR, "llm", "skills", "file.md")) - Do not use
import.meta.url/fileURLToPathto resolve file paths — the server is bundled into CJS for production, soimport.meta.urlwill not point to the source directory - Do not use
__dirnamewith relative paths from source files — after bundling,__dirnamepoints to the bundle output, not the original source tree
MCP Server
- Trilium exposes an MCP (Model Context Protocol) server at
http://localhost:8080/mcp, configured in.mcp.json - The MCP server is only available when the Trilium server is running (
pnpm run server:start) - It provides tools for reading, searching, and modifying notes directly from the AI assistant
- Use it to interact with actual note data when developing or debugging note-related features
Build System Notes
- Uses pnpm for monorepo management
- Vite for fast development builds
- ESBuild for production optimization
- pnpm workspaces for dependency management
- Docker support with multi-stage builds