mirror of
https://github.com/zadam/trilium.git
synced 2025-11-08 15:39:02 +01:00
Add comprehensive technical and architectural documentation
Co-authored-by: eliandoran <21236836+eliandoran@users.noreply.github.com>
This commit is contained in:
parent
3e0d1bfa44
commit
154492e454
1016
docs/ARCHITECTURE.md
vendored
Normal file
1016
docs/ARCHITECTURE.md
vendored
Normal file
File diff suppressed because it is too large
Load Diff
736
docs/DATABASE.md
vendored
Normal file
736
docs/DATABASE.md
vendored
Normal file
@ -0,0 +1,736 @@
|
|||||||
|
# Trilium Database Architecture
|
||||||
|
|
||||||
|
> **Related:** [ARCHITECTURE.md](ARCHITECTURE.md) | [Database Schema](Developer%20Guide/Developer%20Guide/Development%20and%20architecture/Database/)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Trilium uses **SQLite** as its embedded database engine, providing a reliable, file-based storage system that requires no separate database server. The database stores all notes, their relationships, metadata, and configuration.
|
||||||
|
|
||||||
|
## Database File
|
||||||
|
|
||||||
|
**Location:**
|
||||||
|
- Desktop: `~/.local/share/trilium-data/document.db` (Linux/Mac) or `%APPDATA%/trilium-data/document.db` (Windows)
|
||||||
|
- Server: Configured via `TRILIUM_DATA_DIR` environment variable
|
||||||
|
- Docker: Mounted volume at `/home/node/trilium-data/`
|
||||||
|
|
||||||
|
**Characteristics:**
|
||||||
|
- Single-file database
|
||||||
|
- Embedded (no server required)
|
||||||
|
- ACID compliant
|
||||||
|
- Cross-platform
|
||||||
|
- Supports up to 281 TB database size
|
||||||
|
- Efficient for 100k+ notes
|
||||||
|
|
||||||
|
## Database Driver
|
||||||
|
|
||||||
|
**Library:** `better-sqlite3`
|
||||||
|
|
||||||
|
**Why better-sqlite3:**
|
||||||
|
- Native performance (C++ bindings)
|
||||||
|
- Synchronous API (simpler code)
|
||||||
|
- Prepared statements
|
||||||
|
- Transaction support
|
||||||
|
- Type safety
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```typescript
|
||||||
|
// apps/server/src/services/sql.ts
|
||||||
|
import Database from 'better-sqlite3'
|
||||||
|
|
||||||
|
const db = new Database('document.db')
|
||||||
|
const stmt = db.prepare('SELECT * FROM notes WHERE noteId = ?')
|
||||||
|
const note = stmt.get(noteId)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Schema Overview
|
||||||
|
|
||||||
|
Schema location: `apps/server/src/assets/db/schema.sql`
|
||||||
|
|
||||||
|
**Entity Tables:**
|
||||||
|
- `notes` - Core note data
|
||||||
|
- `branches` - Tree relationships
|
||||||
|
- `attributes` - Metadata (labels/relations)
|
||||||
|
- `revisions` - Version history
|
||||||
|
- `attachments` - File attachments
|
||||||
|
- `blobs` - Binary content storage
|
||||||
|
|
||||||
|
**System Tables:**
|
||||||
|
- `options` - Application configuration
|
||||||
|
- `entity_changes` - Change tracking for sync
|
||||||
|
- `recent_notes` - Recently accessed notes
|
||||||
|
- `etapi_tokens` - API authentication tokens
|
||||||
|
- `user_data` - User credentials
|
||||||
|
- `sessions` - Web session storage
|
||||||
|
|
||||||
|
## Entity Tables
|
||||||
|
|
||||||
|
### Notes Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE notes (
|
||||||
|
noteId TEXT NOT NULL PRIMARY KEY,
|
||||||
|
title TEXT NOT NULL DEFAULT "note",
|
||||||
|
isProtected INT NOT NULL DEFAULT 0,
|
||||||
|
type TEXT NOT NULL DEFAULT 'text',
|
||||||
|
mime TEXT NOT NULL DEFAULT 'text/html',
|
||||||
|
blobId TEXT DEFAULT NULL,
|
||||||
|
isDeleted INT NOT NULL DEFAULT 0,
|
||||||
|
deleteId TEXT DEFAULT NULL,
|
||||||
|
dateCreated TEXT NOT NULL,
|
||||||
|
dateModified TEXT NOT NULL,
|
||||||
|
utcDateCreated TEXT NOT NULL,
|
||||||
|
utcDateModified TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for performance
|
||||||
|
CREATE INDEX IDX_notes_title ON notes (title);
|
||||||
|
CREATE INDEX IDX_notes_type ON notes (type);
|
||||||
|
CREATE INDEX IDX_notes_dateCreated ON notes (dateCreated);
|
||||||
|
CREATE INDEX IDX_notes_dateModified ON notes (dateModified);
|
||||||
|
CREATE INDEX IDX_notes_utcDateModified ON notes (utcDateModified);
|
||||||
|
CREATE INDEX IDX_notes_blobId ON notes (blobId);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Field Descriptions:**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `noteId` | TEXT | Unique identifier (UUID or custom) |
|
||||||
|
| `title` | TEXT | Note title (displayed in tree) |
|
||||||
|
| `isProtected` | INT | 1 if encrypted, 0 if not |
|
||||||
|
| `type` | TEXT | Note type: text, code, file, image, etc. |
|
||||||
|
| `mime` | TEXT | MIME type: text/html, application/json, etc. |
|
||||||
|
| `blobId` | TEXT | Reference to content in blobs table |
|
||||||
|
| `isDeleted` | INT | Soft delete flag |
|
||||||
|
| `deleteId` | TEXT | Unique delete operation ID |
|
||||||
|
| `dateCreated` | TEXT | Creation date (local timezone) |
|
||||||
|
| `dateModified` | TEXT | Last modified (local timezone) |
|
||||||
|
| `utcDateCreated` | TEXT | Creation date (UTC) |
|
||||||
|
| `utcDateModified` | TEXT | Last modified (UTC) |
|
||||||
|
|
||||||
|
**Note Types:**
|
||||||
|
- `text` - Rich text with HTML
|
||||||
|
- `code` - Source code
|
||||||
|
- `file` - Binary file
|
||||||
|
- `image` - Image file
|
||||||
|
- `search` - Saved search
|
||||||
|
- `render` - Custom HTML rendering
|
||||||
|
- `relation-map` - Relationship diagram
|
||||||
|
- `canvas` - Excalidraw drawing
|
||||||
|
- `mermaid` - Mermaid diagram
|
||||||
|
- `book` - Container for documentation
|
||||||
|
- `web-view` - Embedded web page
|
||||||
|
- `mindmap` - Mind map
|
||||||
|
- `geomap` - Geographical map
|
||||||
|
|
||||||
|
### Branches Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE branches (
|
||||||
|
branchId TEXT NOT NULL PRIMARY KEY,
|
||||||
|
noteId TEXT NOT NULL,
|
||||||
|
parentNoteId TEXT NOT NULL,
|
||||||
|
notePosition INTEGER NOT NULL,
|
||||||
|
prefix TEXT,
|
||||||
|
isExpanded INTEGER NOT NULL DEFAULT 0,
|
||||||
|
isDeleted INTEGER NOT NULL DEFAULT 0,
|
||||||
|
deleteId TEXT DEFAULT NULL,
|
||||||
|
utcDateModified TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX IDX_branches_noteId_parentNoteId ON branches (noteId, parentNoteId);
|
||||||
|
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Field Descriptions:**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `branchId` | TEXT | Unique identifier for this branch |
|
||||||
|
| `noteId` | TEXT | Child note ID |
|
||||||
|
| `parentNoteId` | TEXT | Parent note ID |
|
||||||
|
| `notePosition` | INT | Sort order among siblings |
|
||||||
|
| `prefix` | TEXT | Optional prefix text (e.g., "Chapter 1:") |
|
||||||
|
| `isExpanded` | INT | Tree expansion state |
|
||||||
|
| `isDeleted` | INT | Soft delete flag |
|
||||||
|
| `deleteId` | TEXT | Delete operation ID |
|
||||||
|
| `utcDateModified` | TEXT | Last modified (UTC) |
|
||||||
|
|
||||||
|
**Key Concepts:**
|
||||||
|
- **Cloning:** A note can have multiple branches (multiple parents)
|
||||||
|
- **Position:** Siblings ordered by `notePosition`
|
||||||
|
- **Prefix:** Display text before note title in tree
|
||||||
|
- **Soft Delete:** Allows sync before permanent deletion
|
||||||
|
|
||||||
|
### Attributes Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE attributes (
|
||||||
|
attributeId TEXT NOT NULL PRIMARY KEY,
|
||||||
|
noteId TEXT NOT NULL,
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
value TEXT DEFAULT '' NOT NULL,
|
||||||
|
position INT DEFAULT 0 NOT NULL,
|
||||||
|
utcDateModified TEXT NOT NULL,
|
||||||
|
isDeleted INT NOT NULL,
|
||||||
|
deleteId TEXT DEFAULT NULL,
|
||||||
|
isInheritable INT DEFAULT 0 NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX IDX_attributes_name_value ON attributes (name, value);
|
||||||
|
CREATE INDEX IDX_attributes_noteId ON attributes (noteId);
|
||||||
|
CREATE INDEX IDX_attributes_value ON attributes (value);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Field Descriptions:**
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `attributeId` | TEXT | Unique identifier |
|
||||||
|
| `noteId` | TEXT | Note this attribute belongs to |
|
||||||
|
| `type` | TEXT | 'label' or 'relation' |
|
||||||
|
| `name` | TEXT | Attribute name |
|
||||||
|
| `value` | TEXT | Attribute value (text for labels, noteId for relations) |
|
||||||
|
| `position` | INT | Display order |
|
||||||
|
| `utcDateModified` | TEXT | Last modified (UTC) |
|
||||||
|
| `isDeleted` | INT | Soft delete flag |
|
||||||
|
| `deleteId` | TEXT | Delete operation ID |
|
||||||
|
| `isInheritable` | INT | Inherited by child notes |
|
||||||
|
|
||||||
|
**Attribute Types:**
|
||||||
|
|
||||||
|
**Labels** (key-value pairs):
|
||||||
|
```sql
|
||||||
|
-- Example: #priority=high
|
||||||
|
INSERT INTO attributes (attributeId, noteId, type, name, value)
|
||||||
|
VALUES ('attr1', 'note123', 'label', 'priority', 'high')
|
||||||
|
```
|
||||||
|
|
||||||
|
**Relations** (links to other notes):
|
||||||
|
```sql
|
||||||
|
-- Example: ~author=[[noteId]]
|
||||||
|
INSERT INTO attributes (attributeId, noteId, type, name, value)
|
||||||
|
VALUES ('attr2', 'note123', 'relation', 'author', 'author-note-id')
|
||||||
|
```
|
||||||
|
|
||||||
|
**Special Attributes:**
|
||||||
|
- `#run=frontendStartup` - Execute script on frontend load
|
||||||
|
- `#run=backendStartup` - Execute script on backend load
|
||||||
|
- `#customWidget` - Custom widget implementation
|
||||||
|
- `#iconClass` - Custom tree icon
|
||||||
|
- `#cssClass` - CSS class for note
|
||||||
|
- `#sorted` - Auto-sort children
|
||||||
|
- `#hideChildrenOverview` - Don't show child list
|
||||||
|
|
||||||
|
### Revisions Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE revisions (
|
||||||
|
revisionId TEXT NOT NULL PRIMARY KEY,
|
||||||
|
noteId TEXT NOT NULL,
|
||||||
|
type TEXT DEFAULT '' NOT NULL,
|
||||||
|
mime TEXT DEFAULT '' NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
isProtected INT NOT NULL DEFAULT 0,
|
||||||
|
blobId TEXT DEFAULT NULL,
|
||||||
|
utcDateLastEdited TEXT NOT NULL,
|
||||||
|
utcDateCreated TEXT NOT NULL,
|
||||||
|
utcDateModified TEXT NOT NULL,
|
||||||
|
dateLastEdited TEXT NOT NULL,
|
||||||
|
dateCreated TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX IDX_revisions_noteId ON revisions (noteId);
|
||||||
|
CREATE INDEX IDX_revisions_utcDateCreated ON revisions (utcDateCreated);
|
||||||
|
CREATE INDEX IDX_revisions_utcDateLastEdited ON revisions (utcDateLastEdited);
|
||||||
|
CREATE INDEX IDX_revisions_blobId ON revisions (blobId);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Revision Strategy:**
|
||||||
|
- Automatic revision created on note modification
|
||||||
|
- Configurable interval (default: daily max)
|
||||||
|
- Stores complete note snapshot
|
||||||
|
- Allows reverting to previous versions
|
||||||
|
- Can be disabled with `#disableVersioning`
|
||||||
|
|
||||||
|
### Attachments Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE attachments (
|
||||||
|
attachmentId TEXT NOT NULL PRIMARY KEY,
|
||||||
|
ownerId TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
mime TEXT NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
isProtected INT NOT NULL DEFAULT 0,
|
||||||
|
position INT DEFAULT 0 NOT NULL,
|
||||||
|
blobId TEXT DEFAULT NULL,
|
||||||
|
dateModified TEXT NOT NULL,
|
||||||
|
utcDateModified TEXT NOT NULL,
|
||||||
|
utcDateScheduledForErasureSince TEXT DEFAULT NULL,
|
||||||
|
isDeleted INT NOT NULL,
|
||||||
|
deleteId TEXT DEFAULT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX IDX_attachments_ownerId_role ON attachments (ownerId, role);
|
||||||
|
CREATE INDEX IDX_attachments_blobId ON attachments (blobId);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Attachment Roles:**
|
||||||
|
- `file` - Regular file attachment
|
||||||
|
- `image` - Image file
|
||||||
|
- `cover-image` - Note cover image
|
||||||
|
- Custom roles for specific purposes
|
||||||
|
|
||||||
|
### Blobs Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE blobs (
|
||||||
|
blobId TEXT NOT NULL PRIMARY KEY,
|
||||||
|
content TEXT NULL DEFAULT NULL,
|
||||||
|
dateModified TEXT NOT NULL,
|
||||||
|
utcDateModified TEXT NOT NULL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Blob Usage:**
|
||||||
|
- Stores actual content (text or binary)
|
||||||
|
- Referenced by notes, revisions, attachments
|
||||||
|
- Deduplication via hash-based blobId
|
||||||
|
- TEXT type stores both text and binary (base64)
|
||||||
|
|
||||||
|
**Content Types:**
|
||||||
|
- **Text notes:** HTML content
|
||||||
|
- **Code notes:** Plain text source code
|
||||||
|
- **Binary notes:** Base64 encoded data
|
||||||
|
- **Protected notes:** Encrypted content
|
||||||
|
|
||||||
|
## System Tables
|
||||||
|
|
||||||
|
### Options Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE options (
|
||||||
|
name TEXT NOT NULL PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
isSynced INTEGER DEFAULT 0 NOT NULL,
|
||||||
|
utcDateModified TEXT NOT NULL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Options:**
|
||||||
|
- `documentId` - Unique installation ID
|
||||||
|
- `dbVersion` - Schema version
|
||||||
|
- `syncVersion` - Sync protocol version
|
||||||
|
- `passwordVerificationHash` - Password verification
|
||||||
|
- `encryptedDataKey` - Encryption key (encrypted)
|
||||||
|
- `theme` - UI theme
|
||||||
|
- Various feature flags and settings
|
||||||
|
|
||||||
|
**Synced Options:**
|
||||||
|
- `isSynced = 1` - Synced across devices
|
||||||
|
- `isSynced = 0` - Local to this installation
|
||||||
|
|
||||||
|
### Entity Changes Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE entity_changes (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
entityName TEXT NOT NULL,
|
||||||
|
entityId TEXT NOT NULL,
|
||||||
|
hash TEXT NOT NULL,
|
||||||
|
isErased INT NOT NULL,
|
||||||
|
changeId TEXT NOT NULL,
|
||||||
|
componentId TEXT NOT NULL,
|
||||||
|
instanceId TEXT NOT NULL,
|
||||||
|
isSynced INTEGER NOT NULL,
|
||||||
|
utcDateChanged TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE UNIQUE INDEX IDX_entityChanges_entityName_entityId
|
||||||
|
ON entity_changes (entityName, entityId);
|
||||||
|
CREATE INDEX IDX_entity_changes_changeId ON entity_changes (changeId);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Purpose:** Track all entity modifications for synchronization
|
||||||
|
|
||||||
|
**Entity Types:**
|
||||||
|
- `notes`
|
||||||
|
- `branches`
|
||||||
|
- `attributes`
|
||||||
|
- `revisions`
|
||||||
|
- `attachments`
|
||||||
|
- `options`
|
||||||
|
- `etapi_tokens`
|
||||||
|
|
||||||
|
### Recent Notes Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE recent_notes (
|
||||||
|
noteId TEXT NOT NULL PRIMARY KEY,
|
||||||
|
notePath TEXT NOT NULL,
|
||||||
|
utcDateCreated TEXT NOT NULL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Purpose:** Track recently accessed notes for quick access
|
||||||
|
|
||||||
|
### Sessions Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE sessions (
|
||||||
|
sid TEXT PRIMARY KEY,
|
||||||
|
sess TEXT NOT NULL,
|
||||||
|
expired TEXT NOT NULL
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Purpose:** HTTP session storage for web interface
|
||||||
|
|
||||||
|
### User Data Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE user_data (
|
||||||
|
tmpID INT PRIMARY KEY,
|
||||||
|
username TEXT,
|
||||||
|
email TEXT,
|
||||||
|
userIDEncryptedDataKey TEXT,
|
||||||
|
userIDVerificationHash TEXT,
|
||||||
|
salt TEXT,
|
||||||
|
derivedKey TEXT,
|
||||||
|
isSetup TEXT DEFAULT "false"
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Purpose:** Store user authentication credentials
|
||||||
|
|
||||||
|
### ETAPI Tokens Table
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE etapi_tokens (
|
||||||
|
etapiTokenId TEXT PRIMARY KEY NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
tokenHash TEXT NOT NULL,
|
||||||
|
utcDateCreated TEXT NOT NULL,
|
||||||
|
utcDateModified TEXT NOT NULL,
|
||||||
|
isDeleted INT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Purpose:** API token authentication for external access
|
||||||
|
|
||||||
|
## Data Relationships
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐
|
||||||
|
│ Notes │
|
||||||
|
└───┬──────────┘
|
||||||
|
│
|
||||||
|
┌───────────┼───────────┐
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
┌────────┐ ┌──────────┐ ┌───────────┐
|
||||||
|
│Branches│ │Attributes│ │Attachments│
|
||||||
|
└────────┘ └──────────┘ └─────┬─────┘
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────┐ │
|
||||||
|
└──────▶│ Blobs │◀────────┘
|
||||||
|
└──────────┘
|
||||||
|
▲
|
||||||
|
│
|
||||||
|
┌────┴─────┐
|
||||||
|
│Revisions │
|
||||||
|
└──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Relationships:**
|
||||||
|
- Notes ↔ Branches (many-to-many via noteId)
|
||||||
|
- Notes → Attributes (one-to-many)
|
||||||
|
- Notes → Blobs (one-to-one)
|
||||||
|
- Notes → Revisions (one-to-many)
|
||||||
|
- Notes → Attachments (one-to-many)
|
||||||
|
- Attachments → Blobs (one-to-one)
|
||||||
|
- Revisions → Blobs (one-to-one)
|
||||||
|
|
||||||
|
## Database Access Patterns
|
||||||
|
|
||||||
|
### Direct SQL Access
|
||||||
|
|
||||||
|
**Location:** `apps/server/src/services/sql.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Execute query (returns rows)
|
||||||
|
const notes = sql.getRows('SELECT * FROM notes WHERE type = ?', ['text'])
|
||||||
|
|
||||||
|
// Execute query (returns single row)
|
||||||
|
const note = sql.getRow('SELECT * FROM notes WHERE noteId = ?', [noteId])
|
||||||
|
|
||||||
|
// Execute statement (no return)
|
||||||
|
sql.execute('UPDATE notes SET title = ? WHERE noteId = ?', [title, noteId])
|
||||||
|
|
||||||
|
// Insert
|
||||||
|
sql.insert('notes', {
|
||||||
|
noteId: 'new-note-id',
|
||||||
|
title: 'New Note',
|
||||||
|
type: 'text',
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
|
||||||
|
// Transactions
|
||||||
|
sql.transactional(() => {
|
||||||
|
sql.execute('UPDATE ...')
|
||||||
|
sql.execute('INSERT ...')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Entity-Based Access (Recommended)
|
||||||
|
|
||||||
|
**Via Becca Cache:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Get entity from cache
|
||||||
|
const note = becca.getNote(noteId)
|
||||||
|
|
||||||
|
// Modify and save
|
||||||
|
note.title = 'Updated Title'
|
||||||
|
note.save() // Writes to database
|
||||||
|
|
||||||
|
// Create new
|
||||||
|
const newNote = becca.createNote({
|
||||||
|
parentNoteId: 'root',
|
||||||
|
title: 'New Note',
|
||||||
|
type: 'text',
|
||||||
|
content: 'Hello World'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
note.markAsDeleted()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Database Migrations
|
||||||
|
|
||||||
|
**Location:** `apps/server/src/migrations/`
|
||||||
|
|
||||||
|
**Migration Files:**
|
||||||
|
- Format: `XXXX_migration_name.sql` or `XXXX_migration_name.js`
|
||||||
|
- Executed in numerical order
|
||||||
|
- Version tracked in `options.dbVersion`
|
||||||
|
|
||||||
|
**SQL Migration Example:**
|
||||||
|
```sql
|
||||||
|
-- 0280_add_new_column.sql
|
||||||
|
ALTER TABLE notes ADD COLUMN newField TEXT DEFAULT NULL;
|
||||||
|
|
||||||
|
UPDATE options SET value = '280' WHERE name = 'dbVersion';
|
||||||
|
```
|
||||||
|
|
||||||
|
**JavaScript Migration Example:**
|
||||||
|
```javascript
|
||||||
|
// 0285_complex_migration.js
|
||||||
|
module.exports = () => {
|
||||||
|
const notes = sql.getRows('SELECT * FROM notes WHERE type = ?', ['old-type'])
|
||||||
|
|
||||||
|
for (const note of notes) {
|
||||||
|
sql.execute('UPDATE notes SET type = ? WHERE noteId = ?',
|
||||||
|
['new-type', note.noteId])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Migration Process:**
|
||||||
|
1. Server checks `dbVersion` on startup
|
||||||
|
2. Compares with latest migration number
|
||||||
|
3. Executes pending migrations in order
|
||||||
|
4. Updates `dbVersion` after each
|
||||||
|
5. Restarts if migrations ran
|
||||||
|
|
||||||
|
## Database Maintenance
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
|
||||||
|
**Full Backup:**
|
||||||
|
```bash
|
||||||
|
# Copy database file
|
||||||
|
cp document.db document.db.backup
|
||||||
|
|
||||||
|
# Or use Trilium's backup feature
|
||||||
|
# Settings → Backup
|
||||||
|
```
|
||||||
|
|
||||||
|
**Automatic Backups:**
|
||||||
|
- Daily backup (configurable)
|
||||||
|
- Stored in `backup/` directory
|
||||||
|
- Retention policy (keep last N backups)
|
||||||
|
|
||||||
|
### Vacuum
|
||||||
|
|
||||||
|
**Purpose:** Reclaim unused space, defragment
|
||||||
|
|
||||||
|
```sql
|
||||||
|
VACUUM;
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to vacuum:**
|
||||||
|
- After deleting many notes
|
||||||
|
- Database file size larger than expected
|
||||||
|
- Performance degradation
|
||||||
|
|
||||||
|
### Integrity Check
|
||||||
|
|
||||||
|
```sql
|
||||||
|
PRAGMA integrity_check;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Result:** "ok" or list of errors
|
||||||
|
|
||||||
|
### Consistency Checks
|
||||||
|
|
||||||
|
**Built-in Consistency Checks:**
|
||||||
|
|
||||||
|
Location: `apps/server/src/services/consistency_checks.ts`
|
||||||
|
|
||||||
|
- Orphaned branches
|
||||||
|
- Missing parent notes
|
||||||
|
- Circular dependencies
|
||||||
|
- Invalid entity references
|
||||||
|
- Blob reference integrity
|
||||||
|
|
||||||
|
**Run Checks:**
|
||||||
|
```typescript
|
||||||
|
// Via API
|
||||||
|
POST /api/consistency-check
|
||||||
|
|
||||||
|
// Or from backend script
|
||||||
|
api.runConsistencyChecks()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimization
|
||||||
|
|
||||||
|
### Indexes
|
||||||
|
|
||||||
|
**Existing Indexes:**
|
||||||
|
- `notes.title` - Fast title searches
|
||||||
|
- `notes.type` - Filter by type
|
||||||
|
- `notes.dateCreated/Modified` - Time-based queries
|
||||||
|
- `branches.noteId_parentNoteId` - Tree navigation
|
||||||
|
- `attributes.name_value` - Attribute searches
|
||||||
|
|
||||||
|
**Query Optimization:**
|
||||||
|
```sql
|
||||||
|
-- Use indexed columns in WHERE clause
|
||||||
|
SELECT * FROM notes WHERE type = 'text' -- Uses index
|
||||||
|
|
||||||
|
-- Avoid functions on indexed columns
|
||||||
|
SELECT * FROM notes WHERE LOWER(title) = 'test' -- No index
|
||||||
|
|
||||||
|
-- Better
|
||||||
|
SELECT * FROM notes WHERE title = 'Test' -- Uses index
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connection Settings
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// apps/server/src/services/sql.ts
|
||||||
|
const db = new Database('document.db', {
|
||||||
|
// Enable WAL mode for better concurrency
|
||||||
|
verbose: console.log
|
||||||
|
})
|
||||||
|
|
||||||
|
db.pragma('journal_mode = WAL')
|
||||||
|
db.pragma('synchronous = NORMAL')
|
||||||
|
db.pragma('cache_size = -64000') // 64MB cache
|
||||||
|
db.pragma('temp_store = MEMORY')
|
||||||
|
```
|
||||||
|
|
||||||
|
**WAL Mode Benefits:**
|
||||||
|
- Better concurrency (readers don't block writers)
|
||||||
|
- Faster commits
|
||||||
|
- More robust
|
||||||
|
|
||||||
|
### Query Performance
|
||||||
|
|
||||||
|
**Use EXPLAIN QUERY PLAN:**
|
||||||
|
```sql
|
||||||
|
EXPLAIN QUERY PLAN
|
||||||
|
SELECT * FROM notes
|
||||||
|
WHERE type = 'text'
|
||||||
|
AND dateCreated > '2025-01-01'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Analyze slow queries:**
|
||||||
|
- Check index usage
|
||||||
|
- Avoid SELECT *
|
||||||
|
- Use prepared statements
|
||||||
|
- Batch operations in transactions
|
||||||
|
|
||||||
|
## Database Size Management
|
||||||
|
|
||||||
|
**Typical Sizes:**
|
||||||
|
- 1,000 notes: ~5-10 MB
|
||||||
|
- 10,000 notes: ~50-100 MB
|
||||||
|
- 100,000 notes: ~500 MB - 1 GB
|
||||||
|
|
||||||
|
**Size Reduction Strategies:**
|
||||||
|
|
||||||
|
1. **Delete old revisions**
|
||||||
|
2. **Remove large attachments**
|
||||||
|
3. **Vacuum database**
|
||||||
|
4. **Compact blobs**
|
||||||
|
5. **Archive old notes**
|
||||||
|
|
||||||
|
**Blob Deduplication:**
|
||||||
|
- Blobs identified by content hash
|
||||||
|
- Identical content shares one blob
|
||||||
|
- Automatic deduplication on insert
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Protected Notes Encryption
|
||||||
|
|
||||||
|
**Encryption Process:**
|
||||||
|
```typescript
|
||||||
|
// Encrypt blob content
|
||||||
|
const encryptedContent = encrypt(content, dataKey)
|
||||||
|
blob.content = encryptedContent
|
||||||
|
|
||||||
|
// Store encrypted
|
||||||
|
sql.insert('blobs', { blobId, content: encryptedContent })
|
||||||
|
```
|
||||||
|
|
||||||
|
**Encryption Details:**
|
||||||
|
- Algorithm: AES-256-CBC
|
||||||
|
- Key derivation: PBKDF2 (10,000 iterations)
|
||||||
|
- Per-note encryption
|
||||||
|
- Master key encrypted with user password
|
||||||
|
|
||||||
|
### SQL Injection Prevention
|
||||||
|
|
||||||
|
**Always use parameterized queries:**
|
||||||
|
```typescript
|
||||||
|
// GOOD - Safe from SQL injection
|
||||||
|
sql.execute('SELECT * FROM notes WHERE title = ?', [userInput])
|
||||||
|
|
||||||
|
// BAD - Vulnerable to SQL injection
|
||||||
|
sql.execute(`SELECT * FROM notes WHERE title = '${userInput}'`)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database File Protection
|
||||||
|
|
||||||
|
**File Permissions:**
|
||||||
|
- Owner read/write only
|
||||||
|
- No group/other access
|
||||||
|
- Located in user-specific directory
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**See Also:**
|
||||||
|
- [ARCHITECTURE.md](ARCHITECTURE.md) - Overall architecture
|
||||||
|
- [Database Schema Files](Developer%20Guide/Developer%20Guide/Development%20and%20architecture/Database/)
|
||||||
|
- [Migration Scripts](../apps/server/src/migrations/)
|
||||||
22
docs/README.md
vendored
22
docs/README.md
vendored
@ -1,4 +1,17 @@
|
|||||||
# Trilium Notes
|
# Trilium Notes Documentation
|
||||||
|
|
||||||
|
## 📚 Technical Documentation
|
||||||
|
|
||||||
|
**NEW:** Comprehensive technical and architectural documentation is now available!
|
||||||
|
|
||||||
|
- **[Technical Documentation Index](TECHNICAL_DOCUMENTATION.md)** - Complete index to all technical docs
|
||||||
|
- **[Architecture Overview](ARCHITECTURE.md)** - System design and core patterns
|
||||||
|
- **[Database Architecture](DATABASE.md)** - Complete database documentation
|
||||||
|
- **[Synchronization](SYNCHRONIZATION.md)** - Sync protocol and implementation
|
||||||
|
- **[Scripting System](SCRIPTING.md)** - User scripting guide and API
|
||||||
|
- **[Security Architecture](SECURITY_ARCHITECTURE.md)** - Security implementation details
|
||||||
|
|
||||||
|
## 📖 User Documentation
|
||||||
|
|
||||||
Please see the [main documentation](index.md) or visit one of our translated versions:
|
Please see the [main documentation](index.md) or visit one of our translated versions:
|
||||||
|
|
||||||
@ -9,4 +22,11 @@ Please see the [main documentation](index.md) or visit one of our translated ver
|
|||||||
- [简体中文](README-ZH_CN.md)
|
- [简体中文](README-ZH_CN.md)
|
||||||
- [繁體中文](README-ZH_TW.md)
|
- [繁體中文](README-ZH_TW.md)
|
||||||
|
|
||||||
|
## 🔧 Developer Documentation
|
||||||
|
|
||||||
|
- [Developer Guide](Developer%20Guide/Developer%20Guide/) - Development environment and contribution guide
|
||||||
|
- [Script API](Script%20API/) - Complete scripting API reference
|
||||||
|
|
||||||
|
## 🔗 Additional Resources
|
||||||
|
|
||||||
For the full application README, please visit our [GitHub repository](https://github.com/triliumnext/trilium).
|
For the full application README, please visit our [GitHub repository](https://github.com/triliumnext/trilium).
|
||||||
734
docs/SCRIPTING.md
vendored
Normal file
734
docs/SCRIPTING.md
vendored
Normal file
@ -0,0 +1,734 @@
|
|||||||
|
# Trilium Scripting System
|
||||||
|
|
||||||
|
> **Related:** [ARCHITECTURE.md](ARCHITECTURE.md) | [Script API Documentation](Script%20API/)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Trilium features a **powerful scripting system** that allows users to extend and customize the application without modifying source code. Scripts are written in JavaScript and can execute both in the **frontend (browser)** and **backend (Node.js)** contexts.
|
||||||
|
|
||||||
|
## Script Types
|
||||||
|
|
||||||
|
### Frontend Scripts
|
||||||
|
|
||||||
|
**Location:** Attached to notes with `#run=frontendStartup` attribute
|
||||||
|
|
||||||
|
**Execution Context:** Browser environment
|
||||||
|
|
||||||
|
**Access:**
|
||||||
|
- Trilium Frontend API
|
||||||
|
- Browser APIs (DOM, localStorage, etc.)
|
||||||
|
- Froca (frontend cache)
|
||||||
|
- UI widgets
|
||||||
|
- No direct file system access
|
||||||
|
|
||||||
|
**Lifecycle:**
|
||||||
|
- `frontendStartup` - Run once when Trilium loads
|
||||||
|
- `frontendReload` - Run on every note context change
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```javascript
|
||||||
|
// Attach to note with #run=frontendStartup
|
||||||
|
const api = window.api
|
||||||
|
|
||||||
|
// Add custom button to toolbar
|
||||||
|
api.addButtonToToolbar({
|
||||||
|
title: 'My Button',
|
||||||
|
icon: 'star',
|
||||||
|
action: () => {
|
||||||
|
api.showMessage('Hello from frontend!')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend Scripts
|
||||||
|
|
||||||
|
**Location:** Attached to notes with `#run=backendStartup` attribute
|
||||||
|
|
||||||
|
**Execution Context:** Node.js server environment
|
||||||
|
|
||||||
|
**Access:**
|
||||||
|
- Trilium Backend API
|
||||||
|
- Node.js APIs (fs, http, etc.)
|
||||||
|
- Becca (backend cache)
|
||||||
|
- Database (SQL)
|
||||||
|
- External libraries (via require)
|
||||||
|
|
||||||
|
**Lifecycle:**
|
||||||
|
- `backendStartup` - Run once when server starts
|
||||||
|
- Event handlers (custom events)
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```javascript
|
||||||
|
// Attach to note with #run=backendStartup
|
||||||
|
const api = require('@triliumnext/api')
|
||||||
|
|
||||||
|
// Listen for note creation
|
||||||
|
api.dayjs // Example: access dayjs library
|
||||||
|
|
||||||
|
api.onNoteCreated((note) => {
|
||||||
|
if (note.title.includes('TODO')) {
|
||||||
|
note.setLabel('priority', 'high')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Render Scripts
|
||||||
|
|
||||||
|
**Location:** Attached to notes with `#customWidget` or similar attributes
|
||||||
|
|
||||||
|
**Purpose:** Custom note rendering/widgets
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```javascript
|
||||||
|
// Custom widget for a note
|
||||||
|
class MyWidget extends api.NoteContextAwareWidget {
|
||||||
|
doRender() {
|
||||||
|
this.$widget = $('<div>')
|
||||||
|
.text('Custom widget content')
|
||||||
|
return this.$widget
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MyWidget
|
||||||
|
```
|
||||||
|
|
||||||
|
## Script API
|
||||||
|
|
||||||
|
### Frontend API
|
||||||
|
|
||||||
|
**Location:** `apps/client/src/services/frontend_script_api.ts`
|
||||||
|
|
||||||
|
**Global Access:** `window.api`
|
||||||
|
|
||||||
|
**Key Methods:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Note Operations
|
||||||
|
api.getNote(noteId) // Get note object
|
||||||
|
api.getBranch(branchId) // Get branch object
|
||||||
|
api.getActiveNote() // Currently displayed note
|
||||||
|
api.openNote(noteId, activateNote) // Open note in UI
|
||||||
|
|
||||||
|
// UI Operations
|
||||||
|
api.showMessage(message) // Show toast notification
|
||||||
|
api.showDialog() // Show modal dialog
|
||||||
|
api.confirm(message) // Show confirmation dialog
|
||||||
|
api.prompt(message, defaultValue) // Show input prompt
|
||||||
|
|
||||||
|
// Tree Operations
|
||||||
|
api.getTree() // Get note tree structure
|
||||||
|
api.expandTree(noteId) // Expand tree branch
|
||||||
|
api.collapseTree(noteId) // Collapse tree branch
|
||||||
|
|
||||||
|
// Search
|
||||||
|
api.searchForNotes(searchQuery) // Search notes
|
||||||
|
api.searchForNote(searchQuery) // Get single note
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
api.openTabWithNote(noteId) // Open note in new tab
|
||||||
|
api.closeActiveTab() // Close current tab
|
||||||
|
api.activateNote(noteId) // Switch to note
|
||||||
|
|
||||||
|
// Attributes
|
||||||
|
api.getAttribute(noteId, type, name) // Get attribute
|
||||||
|
api.getAttributes(noteId, type, name) // Get all matching attributes
|
||||||
|
|
||||||
|
// Custom Widgets
|
||||||
|
api.addButtonToToolbar(def) // Add toolbar button
|
||||||
|
api.addCustomWidget(def) // Add custom widget
|
||||||
|
|
||||||
|
// Events
|
||||||
|
api.runOnNoteOpened(callback) // Note opened event
|
||||||
|
api.runOnNoteContentChange(callback) // Content changed event
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
api.dayjs // Date/time library
|
||||||
|
api.formatDate(date) // Format date
|
||||||
|
api.log(message) // Console log
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend API
|
||||||
|
|
||||||
|
**Location:** `apps/server/src/services/backend_script_api.ts`
|
||||||
|
|
||||||
|
**Access:** `require('@triliumnext/api')` or global `api`
|
||||||
|
|
||||||
|
**Key Methods:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Note Operations
|
||||||
|
api.getNote(noteId) // Get note from Becca
|
||||||
|
api.getNoteWithContent(noteId) // Get note with content
|
||||||
|
api.createNote(parentNoteId, title) // Create new note
|
||||||
|
api.deleteNote(noteId) // Delete note
|
||||||
|
|
||||||
|
// Branch Operations
|
||||||
|
api.getBranch(branchId) // Get branch
|
||||||
|
api.createBranch(noteId, parentNoteId) // Create branch (clone)
|
||||||
|
|
||||||
|
// Attribute Operations
|
||||||
|
api.getAttribute(noteId, type, name) // Get attribute
|
||||||
|
api.createAttribute(noteId, type, name, value) // Create attribute
|
||||||
|
|
||||||
|
// Database Access
|
||||||
|
api.sql.getRow(query, params) // Execute SQL query (single row)
|
||||||
|
api.sql.getRows(query, params) // Execute SQL query (multiple rows)
|
||||||
|
api.sql.execute(query, params) // Execute SQL statement
|
||||||
|
|
||||||
|
// Events
|
||||||
|
api.onNoteCreated(callback) // Note created event
|
||||||
|
api.onNoteUpdated(callback) // Note updated event
|
||||||
|
api.onNoteDeleted(callback) // Note deleted event
|
||||||
|
api.onAttributeCreated(callback) // Attribute created event
|
||||||
|
|
||||||
|
// Search
|
||||||
|
api.searchForNotes(searchQuery) // Search notes
|
||||||
|
|
||||||
|
// Date/Time
|
||||||
|
api.dayjs // Date/time library
|
||||||
|
api.now() // Current date/time
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
api.log(message) // Log message
|
||||||
|
api.error(message) // Log error
|
||||||
|
|
||||||
|
// External Communication
|
||||||
|
api.axios // HTTP client library
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
api.backup.backupNow() // Trigger backup
|
||||||
|
api.export.exportSubtree(noteId) // Export notes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Script Attributes
|
||||||
|
|
||||||
|
### Execute Attributes
|
||||||
|
|
||||||
|
- `#run=frontendStartup` - Execute on frontend startup
|
||||||
|
- `#run=backendStartup` - Execute on backend startup
|
||||||
|
- `#run=hourly` - Execute every hour
|
||||||
|
- `#run=daily` - Execute daily
|
||||||
|
|
||||||
|
### Widget Attributes
|
||||||
|
|
||||||
|
- `#customWidget` - Custom note widget
|
||||||
|
- `#widget` - Standard widget integration
|
||||||
|
|
||||||
|
### Other Attributes
|
||||||
|
|
||||||
|
- `#disableVersioning` - Disable automatic versioning for this note
|
||||||
|
- `#hideChildrenOverview` - Hide children in overview
|
||||||
|
- `#iconClass` - Custom icon for note
|
||||||
|
|
||||||
|
## Entity Classes
|
||||||
|
|
||||||
|
### Frontend Entities
|
||||||
|
|
||||||
|
**FNote** (`apps/client/src/entities/fnote.ts`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class FNote {
|
||||||
|
noteId: string
|
||||||
|
title: string
|
||||||
|
type: string
|
||||||
|
mime: string
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
getParentNotes(): FNote[]
|
||||||
|
getChildNotes(): FNote[]
|
||||||
|
getBranches(): FBranch[]
|
||||||
|
|
||||||
|
// Attributes
|
||||||
|
getAttribute(type, name): FAttribute
|
||||||
|
getAttributes(type?, name?): FAttribute[]
|
||||||
|
hasLabel(name): boolean
|
||||||
|
getLabelValue(name): string
|
||||||
|
|
||||||
|
// Content
|
||||||
|
getContent(): Promise<string>
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
open(): void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**FBranch**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class FBranch {
|
||||||
|
branchId: string
|
||||||
|
noteId: string
|
||||||
|
parentNoteId: string
|
||||||
|
prefix: string
|
||||||
|
notePosition: number
|
||||||
|
|
||||||
|
getNote(): FNote
|
||||||
|
getParentNote(): FNote
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**FAttribute**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class FAttribute {
|
||||||
|
attributeId: string
|
||||||
|
noteId: string
|
||||||
|
type: 'label' | 'relation'
|
||||||
|
name: string
|
||||||
|
value: string
|
||||||
|
|
||||||
|
getNote(): FNote
|
||||||
|
getTargetNote(): FNote // For relations
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend Entities
|
||||||
|
|
||||||
|
**BNote** (`apps/server/src/becca/entities/bnote.ts`)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class BNote {
|
||||||
|
noteId: string
|
||||||
|
title: string
|
||||||
|
type: string
|
||||||
|
mime: string
|
||||||
|
isProtected: boolean
|
||||||
|
|
||||||
|
// Content
|
||||||
|
getContent(): string | Buffer
|
||||||
|
setContent(content: string | Buffer): void
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
getParentNotes(): BNote[]
|
||||||
|
getChildNotes(): BNote[]
|
||||||
|
getBranches(): BBranch[]
|
||||||
|
|
||||||
|
// Attributes
|
||||||
|
getAttribute(type, name): BAttribute
|
||||||
|
getAttributes(type?, name?): BAttribute[]
|
||||||
|
setLabel(name, value): BAttribute
|
||||||
|
setRelation(name, targetNoteId): BAttribute
|
||||||
|
hasLabel(name): boolean
|
||||||
|
getLabelValue(name): string
|
||||||
|
|
||||||
|
// Operations
|
||||||
|
save(): void
|
||||||
|
markAsDeleted(): void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**BBranch**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class BBranch {
|
||||||
|
branchId: string
|
||||||
|
noteId: string
|
||||||
|
parentNoteId: string
|
||||||
|
prefix: string
|
||||||
|
notePosition: number
|
||||||
|
|
||||||
|
getNote(): BNote
|
||||||
|
getParentNote(): BNote
|
||||||
|
save(): void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**BAttribute**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class BAttribute {
|
||||||
|
attributeId: string
|
||||||
|
noteId: string
|
||||||
|
type: 'label' | 'relation'
|
||||||
|
name: string
|
||||||
|
value: string
|
||||||
|
|
||||||
|
getNote(): BNote
|
||||||
|
getTargetNote(): BNote // For relations
|
||||||
|
save(): void
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Script Examples
|
||||||
|
|
||||||
|
### Frontend Examples
|
||||||
|
|
||||||
|
**1. Custom Toolbar Button**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// #run=frontendStartup
|
||||||
|
api.addButtonToToolbar({
|
||||||
|
title: 'Export to PDF',
|
||||||
|
icon: 'file-export',
|
||||||
|
action: async () => {
|
||||||
|
const note = api.getActiveNote()
|
||||||
|
if (note) {
|
||||||
|
await api.runOnBackend('exportToPdf', [note.noteId])
|
||||||
|
api.showMessage('Export started')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Auto-Save Reminder**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// #run=frontendStartup
|
||||||
|
let saveTimer
|
||||||
|
api.runOnNoteContentChange(() => {
|
||||||
|
clearTimeout(saveTimer)
|
||||||
|
saveTimer = setTimeout(() => {
|
||||||
|
api.showMessage('Remember to save your work!')
|
||||||
|
}, 300000) // 5 minutes
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Note Statistics Widget**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// #customWidget
|
||||||
|
class StatsWidget extends api.NoteContextAwareWidget {
|
||||||
|
doRender() {
|
||||||
|
this.$widget = $('<div class="stats-widget">')
|
||||||
|
return this.$widget
|
||||||
|
}
|
||||||
|
|
||||||
|
async refreshWithNote(note) {
|
||||||
|
const content = await note.getContent()
|
||||||
|
const words = content.split(/\s+/).length
|
||||||
|
const chars = content.length
|
||||||
|
|
||||||
|
this.$widget.html(`
|
||||||
|
<div>Words: ${words}</div>
|
||||||
|
<div>Characters: ${chars}</div>
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = StatsWidget
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend Examples
|
||||||
|
|
||||||
|
**1. Auto-Tagging on Note Creation**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// #run=backendStartup
|
||||||
|
api.onNoteCreated((note) => {
|
||||||
|
// Auto-tag TODO notes
|
||||||
|
if (note.title.includes('TODO')) {
|
||||||
|
note.setLabel('type', 'todo')
|
||||||
|
note.setLabel('priority', 'normal')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-tag meeting notes by date
|
||||||
|
if (note.title.match(/Meeting \d{4}-\d{2}-\d{2}/)) {
|
||||||
|
note.setLabel('type', 'meeting')
|
||||||
|
const dateMatch = note.title.match(/(\d{4}-\d{2}-\d{2})/)
|
||||||
|
if (dateMatch) {
|
||||||
|
note.setLabel('date', dateMatch[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Daily Backup Reminder**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// #run=daily
|
||||||
|
const todayNote = api.getTodayNote()
|
||||||
|
todayNote.setLabel('backupDone', 'false')
|
||||||
|
|
||||||
|
// Create reminder note
|
||||||
|
api.createNote(todayNote.noteId, '🔔 Backup Reminder', {
|
||||||
|
content: 'Remember to verify today\'s backup!',
|
||||||
|
type: 'text'
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. External API Integration**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// #run=backendStartup
|
||||||
|
api.onNoteCreated(async (note) => {
|
||||||
|
// Sync new notes to external service
|
||||||
|
if (note.hasLabel('sync-external')) {
|
||||||
|
try {
|
||||||
|
await api.axios.post('https://external-api.com/sync', {
|
||||||
|
noteId: note.noteId,
|
||||||
|
title: note.title,
|
||||||
|
content: note.getContent()
|
||||||
|
})
|
||||||
|
note.setLabel('lastSync', api.dayjs().format())
|
||||||
|
} catch (error) {
|
||||||
|
api.log('Sync failed: ' + error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. Database Cleanup**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// #run=weekly
|
||||||
|
// Clean up old revisions
|
||||||
|
const cutoffDate = api.dayjs().subtract(90, 'days').format()
|
||||||
|
|
||||||
|
const oldRevisions = api.sql.getRows(`
|
||||||
|
SELECT revisionId FROM revisions
|
||||||
|
WHERE utcDateCreated < ?
|
||||||
|
`, [cutoffDate])
|
||||||
|
|
||||||
|
api.log(`Deleting ${oldRevisions.length} old revisions`)
|
||||||
|
|
||||||
|
for (const row of oldRevisions) {
|
||||||
|
api.sql.execute('DELETE FROM revisions WHERE revisionId = ?', [row.revisionId])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Script Storage
|
||||||
|
|
||||||
|
**Storage Location:** Scripts are stored as regular notes
|
||||||
|
|
||||||
|
**Identifying Scripts:**
|
||||||
|
- Have `#run` attribute or `#customWidget` attribute
|
||||||
|
- Type is typically `code` with MIME `application/javascript`
|
||||||
|
|
||||||
|
**Script Note Structure:**
|
||||||
|
```
|
||||||
|
📁 Scripts (folder note)
|
||||||
|
├── 📜 Frontend Scripts
|
||||||
|
│ ├── Custom Toolbar Button (#run=frontendStartup)
|
||||||
|
│ └── Statistics Widget (#customWidget)
|
||||||
|
└── 📜 Backend Scripts
|
||||||
|
├── Auto-Tagger (#run=backendStartup)
|
||||||
|
└── Daily Backup (#run=daily)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Script Execution
|
||||||
|
|
||||||
|
### Frontend Execution
|
||||||
|
|
||||||
|
**Timing:**
|
||||||
|
1. Trilium frontend loads
|
||||||
|
2. Froca cache initializes
|
||||||
|
3. Script notes with `#run=frontendStartup` are found
|
||||||
|
4. Scripts execute in dependency order
|
||||||
|
|
||||||
|
**Isolation:**
|
||||||
|
- Each script runs in separate context
|
||||||
|
- Shared `window.api` object
|
||||||
|
- Can access global window object
|
||||||
|
|
||||||
|
### Backend Execution
|
||||||
|
|
||||||
|
**Timing:**
|
||||||
|
1. Server starts
|
||||||
|
2. Becca cache loads
|
||||||
|
3. Script notes with `#run=backendStartup` are found
|
||||||
|
4. Scripts execute in dependency order
|
||||||
|
|
||||||
|
**Isolation:**
|
||||||
|
- Each script is a separate module
|
||||||
|
- Can require Node.js modules
|
||||||
|
- Shared `api` global
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
```javascript
|
||||||
|
try {
|
||||||
|
// Script code
|
||||||
|
} catch (error) {
|
||||||
|
api.showError('Script error: ' + error.message)
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
```javascript
|
||||||
|
try {
|
||||||
|
// Script code
|
||||||
|
} catch (error) {
|
||||||
|
api.log('Script error: ' + error.message)
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Frontend Scripts
|
||||||
|
|
||||||
|
**Risks:**
|
||||||
|
- Can access all notes via Froca
|
||||||
|
- Can manipulate DOM
|
||||||
|
- Can make API calls
|
||||||
|
- Limited by browser security model
|
||||||
|
|
||||||
|
**Mitigations:**
|
||||||
|
- User must trust scripts they add
|
||||||
|
- Scripts run with user privileges
|
||||||
|
- No access to file system
|
||||||
|
|
||||||
|
### Backend Scripts
|
||||||
|
|
||||||
|
**Risks:**
|
||||||
|
- Full Node.js access
|
||||||
|
- Can execute system commands
|
||||||
|
- Can access file system
|
||||||
|
- Can make network requests
|
||||||
|
|
||||||
|
**Mitigations:**
|
||||||
|
- Scripts are user-created (trusted)
|
||||||
|
- Single-user model (no privilege escalation)
|
||||||
|
- Review scripts before adding `#run` attribute
|
||||||
|
|
||||||
|
### Best Practices
|
||||||
|
|
||||||
|
1. **Review script code** before adding execution attributes
|
||||||
|
2. **Use specific attributes** rather than wildcard searches
|
||||||
|
3. **Avoid eval()** and dynamic code execution
|
||||||
|
4. **Validate inputs** in scripts
|
||||||
|
5. **Handle errors** gracefully
|
||||||
|
6. **Log important actions** for audit trail
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### Optimization Tips
|
||||||
|
|
||||||
|
**1. Cache Results:**
|
||||||
|
```javascript
|
||||||
|
// Bad: Re-query on every call
|
||||||
|
function getConfig() {
|
||||||
|
return api.getNote('config').getContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good: Cache the result
|
||||||
|
let cachedConfig
|
||||||
|
function getConfig() {
|
||||||
|
if (!cachedConfig) {
|
||||||
|
cachedConfig = api.getNote('config').getContent()
|
||||||
|
}
|
||||||
|
return cachedConfig
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. Use Efficient Queries:**
|
||||||
|
```javascript
|
||||||
|
// Bad: Load all notes and filter
|
||||||
|
const todos = api.searchForNotes('#type=todo')
|
||||||
|
|
||||||
|
// Good: Use specific search
|
||||||
|
const todos = api.searchForNotes('#type=todo #status=pending')
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. Batch Operations:**
|
||||||
|
```javascript
|
||||||
|
// Bad: Save after each change
|
||||||
|
notes.forEach(note => {
|
||||||
|
note.title = 'Updated'
|
||||||
|
note.save()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Good: Batch changes
|
||||||
|
notes.forEach(note => {
|
||||||
|
note.title = 'Updated'
|
||||||
|
})
|
||||||
|
// Save happens in batch
|
||||||
|
```
|
||||||
|
|
||||||
|
**4. Debounce Event Handlers:**
|
||||||
|
```javascript
|
||||||
|
let timeout
|
||||||
|
api.runOnNoteContentChange(() => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
// Process change
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Debugging Scripts
|
||||||
|
|
||||||
|
### Frontend Debugging
|
||||||
|
|
||||||
|
**Browser DevTools:**
|
||||||
|
```javascript
|
||||||
|
console.log('Debug info:', data)
|
||||||
|
debugger // Breakpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
**Trilium Log:**
|
||||||
|
```javascript
|
||||||
|
api.log('Script executed')
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend Debugging
|
||||||
|
|
||||||
|
**Console Output:**
|
||||||
|
```javascript
|
||||||
|
console.log('Backend debug:', data)
|
||||||
|
api.log('Script log message')
|
||||||
|
```
|
||||||
|
|
||||||
|
**Inspect Becca:**
|
||||||
|
```javascript
|
||||||
|
api.log('Note count:', Object.keys(api.becca.notes).length)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Advanced Topics
|
||||||
|
|
||||||
|
### Custom Note Types
|
||||||
|
|
||||||
|
Scripts can implement custom note type handlers:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Register custom type
|
||||||
|
api.registerNoteType({
|
||||||
|
type: 'mytype',
|
||||||
|
mime: 'application/x-mytype',
|
||||||
|
renderNote: (note) => {
|
||||||
|
// Custom rendering
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### External Libraries
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
```javascript
|
||||||
|
// Load external library
|
||||||
|
const myLib = await import('https://cdn.example.com/lib.js')
|
||||||
|
```
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
```javascript
|
||||||
|
// Use Node.js require
|
||||||
|
const fs = require('fs')
|
||||||
|
const axios = require('axios')
|
||||||
|
```
|
||||||
|
|
||||||
|
### State Persistence
|
||||||
|
|
||||||
|
**Frontend:**
|
||||||
|
```javascript
|
||||||
|
// Use localStorage
|
||||||
|
localStorage.setItem('myScript:data', JSON.stringify(data))
|
||||||
|
const data = JSON.parse(localStorage.getItem('myScript:data'))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Backend:**
|
||||||
|
```javascript
|
||||||
|
// Store in special note
|
||||||
|
const stateNote = api.getNote('script-state-note')
|
||||||
|
stateNote.setContent(JSON.stringify(data))
|
||||||
|
|
||||||
|
const data = JSON.parse(stateNote.getContent())
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**See Also:**
|
||||||
|
- [Script API Documentation](Script%20API/) - Complete API reference
|
||||||
|
- [Advanced Showcases](https://triliumnext.github.io/Docs/Wiki/advanced-showcases) - Example scripts
|
||||||
|
- [ARCHITECTURE.md](ARCHITECTURE.md) - Overall architecture
|
||||||
834
docs/SECURITY_ARCHITECTURE.md
vendored
Normal file
834
docs/SECURITY_ARCHITECTURE.md
vendored
Normal file
@ -0,0 +1,834 @@
|
|||||||
|
# Trilium Security Architecture
|
||||||
|
|
||||||
|
> **Related:** [ARCHITECTURE.md](ARCHITECTURE.md) | [SECURITY.md](../SECURITY.md)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Trilium implements a **defense-in-depth security model** with multiple layers of protection for user data. The security architecture covers authentication, authorization, encryption, input sanitization, and secure communication.
|
||||||
|
|
||||||
|
## Security Principles
|
||||||
|
|
||||||
|
1. **Data Privacy**: User data is protected at rest and in transit
|
||||||
|
2. **Encryption**: Per-note encryption for sensitive content
|
||||||
|
3. **Authentication**: Multiple authentication methods supported
|
||||||
|
4. **Authorization**: Single-user model with granular protected sessions
|
||||||
|
5. **Input Validation**: All user input sanitized
|
||||||
|
6. **Secure Defaults**: Security features enabled by default
|
||||||
|
7. **Transparency**: Open source allows security audits
|
||||||
|
|
||||||
|
## Threat Model
|
||||||
|
|
||||||
|
### Threats Considered
|
||||||
|
|
||||||
|
1. **Unauthorized Access**
|
||||||
|
- Physical access to device
|
||||||
|
- Network eavesdropping
|
||||||
|
- Stolen credentials
|
||||||
|
- Session hijacking
|
||||||
|
|
||||||
|
2. **Data Exfiltration**
|
||||||
|
- Malicious scripts
|
||||||
|
- XSS attacks
|
||||||
|
- SQL injection
|
||||||
|
- CSRF attacks
|
||||||
|
|
||||||
|
3. **Data Corruption**
|
||||||
|
- Malicious modifications
|
||||||
|
- Database tampering
|
||||||
|
- Sync conflicts
|
||||||
|
|
||||||
|
4. **Privacy Leaks**
|
||||||
|
- Unencrypted backups
|
||||||
|
- Search indexing
|
||||||
|
- Temporary files
|
||||||
|
- Memory dumps
|
||||||
|
|
||||||
|
### Out of Scope
|
||||||
|
|
||||||
|
- Nation-state attackers
|
||||||
|
- Zero-day vulnerabilities in dependencies
|
||||||
|
- Hardware vulnerabilities (Spectre, Meltdown)
|
||||||
|
- Physical access with unlimited time
|
||||||
|
- Quantum computing attacks
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
### Password Authentication
|
||||||
|
|
||||||
|
**Implementation:** `apps/server/src/services/password.ts`
|
||||||
|
|
||||||
|
**Password Storage:**
|
||||||
|
```typescript
|
||||||
|
// Password is never stored directly
|
||||||
|
const salt = crypto.randomBytes(32)
|
||||||
|
const derivedKey = crypto.pbkdf2Sync(password, salt, 10000, 32, 'sha256')
|
||||||
|
const verificationHash = crypto.createHash('sha256')
|
||||||
|
.update(derivedKey)
|
||||||
|
.digest('hex')
|
||||||
|
|
||||||
|
// Store only salt and verification hash
|
||||||
|
sql.insert('user_data', {
|
||||||
|
salt: salt.toString('hex'),
|
||||||
|
derivedKey: derivedKey.toString('hex') // Used for encryption
|
||||||
|
})
|
||||||
|
|
||||||
|
sql.insert('options', {
|
||||||
|
name: 'passwordVerificationHash',
|
||||||
|
value: verificationHash
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Password Requirements:**
|
||||||
|
- Minimum length: 4 characters (configurable)
|
||||||
|
- No maximum length
|
||||||
|
- All characters allowed
|
||||||
|
- Can be changed by user
|
||||||
|
|
||||||
|
**Login Process:**
|
||||||
|
```typescript
|
||||||
|
// 1. User submits password
|
||||||
|
POST /api/login/password
|
||||||
|
Body: { password: "user-password" }
|
||||||
|
|
||||||
|
// 2. Server derives key
|
||||||
|
const derivedKey = crypto.pbkdf2Sync(password, salt, 10000, 32, 'sha256')
|
||||||
|
|
||||||
|
// 3. Verify against stored hash
|
||||||
|
const verificationHash = crypto.createHash('sha256')
|
||||||
|
.update(derivedKey)
|
||||||
|
.digest('hex')
|
||||||
|
|
||||||
|
if (verificationHash === storedHash) {
|
||||||
|
// 4. Create session
|
||||||
|
req.session.loggedIn = true
|
||||||
|
req.session.regenerate()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### TOTP (Two-Factor Authentication)
|
||||||
|
|
||||||
|
**Implementation:** `apps/server/src/routes/api/login.ts`
|
||||||
|
|
||||||
|
**Setup Process:**
|
||||||
|
```typescript
|
||||||
|
// 1. Generate secret
|
||||||
|
const secret = speakeasy.generateSecret({
|
||||||
|
name: `Trilium (${username})`,
|
||||||
|
length: 32
|
||||||
|
})
|
||||||
|
|
||||||
|
// 2. Store encrypted secret
|
||||||
|
const encryptedSecret = encrypt(secret.base32, dataKey)
|
||||||
|
sql.insert('options', {
|
||||||
|
name: 'totpSecret',
|
||||||
|
value: encryptedSecret
|
||||||
|
})
|
||||||
|
|
||||||
|
// 3. Generate QR code
|
||||||
|
const qrCodeUrl = secret.otpauth_url
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verification:**
|
||||||
|
```typescript
|
||||||
|
// User submits TOTP token
|
||||||
|
POST /api/login/totp
|
||||||
|
Body: { token: "123456" }
|
||||||
|
|
||||||
|
// Verify token
|
||||||
|
const secret = decrypt(encryptedSecret, dataKey)
|
||||||
|
const verified = speakeasy.totp.verify({
|
||||||
|
secret: secret,
|
||||||
|
encoding: 'base32',
|
||||||
|
token: token,
|
||||||
|
window: 1 // Allow 1 time step tolerance
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### OpenID Connect
|
||||||
|
|
||||||
|
**Implementation:** `apps/server/src/routes/api/login.ts`
|
||||||
|
|
||||||
|
**Supported Providers:**
|
||||||
|
- Any OpenID Connect compatible provider
|
||||||
|
- Google, GitHub, Auth0, etc.
|
||||||
|
|
||||||
|
**Flow:**
|
||||||
|
```typescript
|
||||||
|
// 1. Redirect to provider
|
||||||
|
GET /api/login/openid
|
||||||
|
|
||||||
|
// 2. Provider redirects back with code
|
||||||
|
GET /api/login/openid/callback?code=...
|
||||||
|
|
||||||
|
// 3. Exchange code for tokens
|
||||||
|
const tokens = await openidClient.callback(redirectUri, req.query)
|
||||||
|
|
||||||
|
// 4. Verify ID token
|
||||||
|
const claims = tokens.claims()
|
||||||
|
|
||||||
|
// 5. Create session
|
||||||
|
req.session.loggedIn = true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Session Management
|
||||||
|
|
||||||
|
**Session Storage:** SQLite database (sessions table)
|
||||||
|
|
||||||
|
**Session Configuration:**
|
||||||
|
```typescript
|
||||||
|
app.use(session({
|
||||||
|
secret: sessionSecret,
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false,
|
||||||
|
rolling: true,
|
||||||
|
cookie: {
|
||||||
|
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
||||||
|
httpOnly: true,
|
||||||
|
secure: isHttps,
|
||||||
|
sameSite: 'lax'
|
||||||
|
},
|
||||||
|
store: new SqliteStore({
|
||||||
|
db: db,
|
||||||
|
table: 'sessions'
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
```
|
||||||
|
|
||||||
|
**Session Invalidation:**
|
||||||
|
- Automatic timeout after inactivity
|
||||||
|
- Manual logout clears session
|
||||||
|
- Server restart invalidates all sessions (optional)
|
||||||
|
|
||||||
|
## Authorization
|
||||||
|
|
||||||
|
### Single-User Model
|
||||||
|
|
||||||
|
**Desktop:**
|
||||||
|
- Single user (owner of device)
|
||||||
|
- No multi-user support
|
||||||
|
- Full access to all notes
|
||||||
|
|
||||||
|
**Server:**
|
||||||
|
- Single user per installation
|
||||||
|
- Authentication required for all operations
|
||||||
|
- No user roles or permissions
|
||||||
|
|
||||||
|
### Protected Sessions
|
||||||
|
|
||||||
|
**Purpose:** Temporary access to encrypted (protected) notes
|
||||||
|
|
||||||
|
**Implementation:** `apps/server/src/services/protected_session.ts`
|
||||||
|
|
||||||
|
**Workflow:**
|
||||||
|
```typescript
|
||||||
|
// 1. User enters password for protected notes
|
||||||
|
POST /api/protected-session/enter
|
||||||
|
Body: { password: "protected-password" }
|
||||||
|
|
||||||
|
// 2. Derive encryption key
|
||||||
|
const protectedDataKey = deriveKey(password)
|
||||||
|
|
||||||
|
// 3. Verify password (decrypt known encrypted value)
|
||||||
|
const decrypted = decrypt(testValue, protectedDataKey)
|
||||||
|
if (decrypted === expectedValue) {
|
||||||
|
// 4. Store in memory (not in session)
|
||||||
|
protectedSessionHolder.setProtectedDataKey(protectedDataKey)
|
||||||
|
|
||||||
|
// 5. Set timeout
|
||||||
|
setTimeout(() => {
|
||||||
|
protectedSessionHolder.clearProtectedDataKey()
|
||||||
|
}, timeout)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Protected Session Timeout:**
|
||||||
|
- Default: 10 minutes (configurable)
|
||||||
|
- Extends on activity
|
||||||
|
- Cleared on browser close
|
||||||
|
- Separate from main session
|
||||||
|
|
||||||
|
### API Authorization
|
||||||
|
|
||||||
|
**Internal API:**
|
||||||
|
- Requires authenticated session
|
||||||
|
- CSRF token validation
|
||||||
|
- Same-origin policy
|
||||||
|
|
||||||
|
**ETAPI (External API):**
|
||||||
|
- Token-based authentication
|
||||||
|
- No session required
|
||||||
|
- Rate limiting
|
||||||
|
|
||||||
|
## Encryption
|
||||||
|
|
||||||
|
### Note Encryption
|
||||||
|
|
||||||
|
**Encryption Algorithm:** AES-256-CBC
|
||||||
|
|
||||||
|
**Key Hierarchy:**
|
||||||
|
```
|
||||||
|
User Password
|
||||||
|
↓ (PBKDF2)
|
||||||
|
Data Key (for protected notes)
|
||||||
|
↓ (AES-256)
|
||||||
|
Protected Note Content
|
||||||
|
```
|
||||||
|
|
||||||
|
**Encryption Process:**
|
||||||
|
```typescript
|
||||||
|
// 1. Generate IV (initialization vector)
|
||||||
|
const iv = crypto.randomBytes(16)
|
||||||
|
|
||||||
|
// 2. Encrypt content
|
||||||
|
const cipher = crypto.createCipheriv('aes-256-cbc', dataKey, iv)
|
||||||
|
let encrypted = cipher.update(content, 'utf8', 'base64')
|
||||||
|
encrypted += cipher.final('base64')
|
||||||
|
|
||||||
|
// 3. Prepend IV to encrypted content
|
||||||
|
const encryptedBlob = iv.toString('base64') + ':' + encrypted
|
||||||
|
|
||||||
|
// 4. Store in database
|
||||||
|
sql.insert('blobs', {
|
||||||
|
blobId: blobId,
|
||||||
|
content: encryptedBlob
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Decryption Process:**
|
||||||
|
```typescript
|
||||||
|
// 1. Split IV and encrypted content
|
||||||
|
const [ivBase64, encryptedData] = encryptedBlob.split(':')
|
||||||
|
const iv = Buffer.from(ivBase64, 'base64')
|
||||||
|
|
||||||
|
// 2. Decrypt
|
||||||
|
const decipher = crypto.createDecipheriv('aes-256-cbc', dataKey, iv)
|
||||||
|
let decrypted = decipher.update(encryptedData, 'base64', 'utf8')
|
||||||
|
decrypted += decipher.final('utf8')
|
||||||
|
|
||||||
|
return decrypted
|
||||||
|
```
|
||||||
|
|
||||||
|
**Protected Note Metadata:**
|
||||||
|
- Title is NOT encrypted (for tree display)
|
||||||
|
- Type and MIME are NOT encrypted
|
||||||
|
- Content IS encrypted
|
||||||
|
- Attributes CAN be encrypted (optional)
|
||||||
|
|
||||||
|
### Data Key Management
|
||||||
|
|
||||||
|
**Master Data Key:**
|
||||||
|
```typescript
|
||||||
|
// Generated once during setup
|
||||||
|
const dataKey = crypto.randomBytes(32) // 256 bits
|
||||||
|
|
||||||
|
// Encrypted with derived key from user password
|
||||||
|
const derivedKey = crypto.pbkdf2Sync(password, salt, 10000, 32, 'sha256')
|
||||||
|
const encryptedDataKey = encrypt(dataKey, derivedKey)
|
||||||
|
|
||||||
|
// Stored in database
|
||||||
|
sql.insert('options', {
|
||||||
|
name: 'encryptedDataKey',
|
||||||
|
value: encryptedDataKey.toString('hex')
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Rotation:**
|
||||||
|
- Not currently supported
|
||||||
|
- Requires re-encrypting all protected notes
|
||||||
|
- Planned for future version
|
||||||
|
|
||||||
|
### Transport Encryption
|
||||||
|
|
||||||
|
**HTTPS:**
|
||||||
|
- Required for server installations (recommended)
|
||||||
|
- TLS 1.2+ only
|
||||||
|
- Strong cipher suites preferred
|
||||||
|
- Certificate validation enabled
|
||||||
|
|
||||||
|
**Desktop:**
|
||||||
|
- Local communication (no network)
|
||||||
|
- No HTTPS required
|
||||||
|
|
||||||
|
### Backup Encryption
|
||||||
|
|
||||||
|
**Database Backups:**
|
||||||
|
- Protected notes remain encrypted in backup
|
||||||
|
- Backup file should be protected separately
|
||||||
|
- Consider encrypting backup storage location
|
||||||
|
|
||||||
|
## Input Sanitization
|
||||||
|
|
||||||
|
### XSS Prevention
|
||||||
|
|
||||||
|
**HTML Sanitization:**
|
||||||
|
|
||||||
|
Location: `apps/client/src/services/dompurify.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
|
|
||||||
|
// Configure DOMPurify
|
||||||
|
DOMPurify.setConfig({
|
||||||
|
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'div', ...],
|
||||||
|
ALLOWED_ATTR: ['href', 'title', 'class', 'id', ...],
|
||||||
|
ALLOW_DATA_ATTR: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// Sanitize HTML before rendering
|
||||||
|
const cleanHtml = DOMPurify.sanitize(userHtml)
|
||||||
|
```
|
||||||
|
|
||||||
|
**CKEditor Configuration:**
|
||||||
|
```typescript
|
||||||
|
// apps/client/src/widgets/type_widgets/text_type_widget.ts
|
||||||
|
ClassicEditor.create(element, {
|
||||||
|
// Restrict allowed content
|
||||||
|
htmlSupport: {
|
||||||
|
allow: [
|
||||||
|
{ name: /./, attributes: true, classes: true, styles: true }
|
||||||
|
],
|
||||||
|
disallow: [
|
||||||
|
{ name: 'script' },
|
||||||
|
{ name: 'iframe', attributes: /^(?!src$).*/ }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Content Security Policy:**
|
||||||
|
```typescript
|
||||||
|
// apps/server/src/main.ts
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
res.setHeader('Content-Security-Policy',
|
||||||
|
"default-src 'self'; " +
|
||||||
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " +
|
||||||
|
"style-src 'self' 'unsafe-inline'; " +
|
||||||
|
"img-src 'self' data: blob:;"
|
||||||
|
)
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### SQL Injection Prevention
|
||||||
|
|
||||||
|
**Parameterized Queries:**
|
||||||
|
```typescript
|
||||||
|
// GOOD - Safe from SQL injection
|
||||||
|
const notes = sql.getRows(
|
||||||
|
'SELECT * FROM notes WHERE title = ?',
|
||||||
|
[userInput]
|
||||||
|
)
|
||||||
|
|
||||||
|
// BAD - Vulnerable to SQL injection
|
||||||
|
const notes = sql.getRows(
|
||||||
|
`SELECT * FROM notes WHERE title = '${userInput}'`
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**ORM Usage:**
|
||||||
|
```typescript
|
||||||
|
// Entity-based access prevents SQL injection
|
||||||
|
const note = becca.getNote(noteId)
|
||||||
|
note.title = userInput // Sanitized by entity
|
||||||
|
note.save() // Parameterized query
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSRF Prevention
|
||||||
|
|
||||||
|
**CSRF Token Validation:**
|
||||||
|
|
||||||
|
Location: `apps/server/src/routes/middleware/csrf.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Generate CSRF token
|
||||||
|
const csrfToken = crypto.randomBytes(32).toString('hex')
|
||||||
|
req.session.csrfToken = csrfToken
|
||||||
|
|
||||||
|
// Validate on state-changing requests
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (['POST', 'PUT', 'DELETE'].includes(req.method)) {
|
||||||
|
const token = req.headers['x-csrf-token']
|
||||||
|
if (token !== req.session.csrfToken) {
|
||||||
|
return res.status(403).json({ error: 'CSRF token mismatch' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Client-Side:**
|
||||||
|
```typescript
|
||||||
|
// apps/client/src/services/server.ts
|
||||||
|
const csrfToken = getCsrfToken()
|
||||||
|
|
||||||
|
fetch('/api/notes', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRF-Token': csrfToken,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Upload Validation
|
||||||
|
|
||||||
|
**Validation:**
|
||||||
|
```typescript
|
||||||
|
// apps/server/src/routes/api/attachments.ts
|
||||||
|
const allowedMimeTypes = [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'application/pdf',
|
||||||
|
// ...
|
||||||
|
]
|
||||||
|
|
||||||
|
if (!allowedMimeTypes.includes(file.mimetype)) {
|
||||||
|
throw new Error('File type not allowed')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file size
|
||||||
|
const maxSize = 100 * 1024 * 1024 // 100 MB
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
throw new Error('File too large')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize filename
|
||||||
|
const sanitizedFilename = path.basename(file.originalname)
|
||||||
|
.replace(/[^a-z0-9.-]/gi, '_')
|
||||||
|
```
|
||||||
|
|
||||||
|
## Network Security
|
||||||
|
|
||||||
|
### HTTPS Configuration
|
||||||
|
|
||||||
|
**Server Setup:**
|
||||||
|
```typescript
|
||||||
|
// apps/server/src/main.ts
|
||||||
|
const httpsOptions = {
|
||||||
|
key: fs.readFileSync('server.key'),
|
||||||
|
cert: fs.readFileSync('server.cert')
|
||||||
|
}
|
||||||
|
|
||||||
|
https.createServer(httpsOptions, app).listen(443)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Certificate Validation:**
|
||||||
|
- Require valid certificates in production
|
||||||
|
- Self-signed certificates allowed for development
|
||||||
|
- Certificate pinning not implemented
|
||||||
|
|
||||||
|
### Secure Headers
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// apps/server/src/main.ts
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
// Prevent clickjacking
|
||||||
|
res.setHeader('X-Frame-Options', 'SAMEORIGIN')
|
||||||
|
|
||||||
|
// Prevent MIME sniffing
|
||||||
|
res.setHeader('X-Content-Type-Options', 'nosniff')
|
||||||
|
|
||||||
|
// XSS protection
|
||||||
|
res.setHeader('X-XSS-Protection', '1; mode=block')
|
||||||
|
|
||||||
|
// Referrer policy
|
||||||
|
res.setHeader('Referrer-Policy', 'same-origin')
|
||||||
|
|
||||||
|
// HTTPS upgrade
|
||||||
|
if (req.secure) {
|
||||||
|
res.setHeader('Strict-Transport-Security', 'max-age=31536000')
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rate Limiting
|
||||||
|
|
||||||
|
**API Rate Limiting:**
|
||||||
|
```typescript
|
||||||
|
// apps/server/src/routes/middleware/rate_limit.ts
|
||||||
|
const rateLimit = require('express-rate-limit')
|
||||||
|
|
||||||
|
const apiLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 1000, // Limit each IP to 1000 requests per window
|
||||||
|
message: 'Too many requests from this IP'
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use('/api/', apiLimiter)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Login Rate Limiting:**
|
||||||
|
```typescript
|
||||||
|
const loginLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000,
|
||||||
|
max: 5, // 5 failed attempts
|
||||||
|
skipSuccessfulRequests: true
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/api/login/password', loginLimiter, loginHandler)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Security
|
||||||
|
|
||||||
|
### Secure Data Deletion
|
||||||
|
|
||||||
|
**Soft Delete:**
|
||||||
|
```typescript
|
||||||
|
// Mark as deleted (sync first)
|
||||||
|
note.isDeleted = 1
|
||||||
|
note.deleteId = generateUUID()
|
||||||
|
note.save()
|
||||||
|
|
||||||
|
// Entity change tracked for sync
|
||||||
|
addEntityChange('notes', noteId, note)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hard Delete (Erase):**
|
||||||
|
```typescript
|
||||||
|
// After sync completed
|
||||||
|
sql.execute('DELETE FROM notes WHERE noteId = ?', [noteId])
|
||||||
|
sql.execute('DELETE FROM branches WHERE noteId = ?', [noteId])
|
||||||
|
sql.execute('DELETE FROM attributes WHERE noteId = ?', [noteId])
|
||||||
|
|
||||||
|
// Mark entity change as erased
|
||||||
|
sql.execute('UPDATE entity_changes SET isErased = 1 WHERE entityId = ?', [noteId])
|
||||||
|
```
|
||||||
|
|
||||||
|
**Blob Cleanup:**
|
||||||
|
```typescript
|
||||||
|
// Find orphaned blobs (not referenced by any note/revision/attachment)
|
||||||
|
const orphanedBlobs = sql.getRows(`
|
||||||
|
SELECT blobId FROM blobs
|
||||||
|
WHERE blobId NOT IN (SELECT blobId FROM notes WHERE blobId IS NOT NULL)
|
||||||
|
AND blobId NOT IN (SELECT blobId FROM revisions WHERE blobId IS NOT NULL)
|
||||||
|
AND blobId NOT IN (SELECT blobId FROM attachments WHERE blobId IS NOT NULL)
|
||||||
|
`)
|
||||||
|
|
||||||
|
// Delete orphaned blobs
|
||||||
|
for (const blob of orphanedBlobs) {
|
||||||
|
sql.execute('DELETE FROM blobs WHERE blobId = ?', [blob.blobId])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Memory Security
|
||||||
|
|
||||||
|
**Protected Data in Memory:**
|
||||||
|
- Protected data keys stored in memory only
|
||||||
|
- Cleared on timeout
|
||||||
|
- Not written to disk
|
||||||
|
- Not in session storage
|
||||||
|
|
||||||
|
**Memory Cleanup:**
|
||||||
|
```typescript
|
||||||
|
// Clear sensitive data
|
||||||
|
const clearSensitiveData = () => {
|
||||||
|
protectedDataKey = null
|
||||||
|
|
||||||
|
// Force garbage collection if available
|
||||||
|
if (global.gc) {
|
||||||
|
global.gc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Temporary Files
|
||||||
|
|
||||||
|
**Secure Temporary Files:**
|
||||||
|
```typescript
|
||||||
|
const tempDir = os.tmpdir()
|
||||||
|
const tempFile = path.join(tempDir, `trilium-${crypto.randomBytes(16).toString('hex')}`)
|
||||||
|
|
||||||
|
// Write temp file
|
||||||
|
fs.writeFileSync(tempFile, data, { mode: 0o600 }) // Owner read/write only
|
||||||
|
|
||||||
|
// Clean up after use
|
||||||
|
fs.unlinkSync(tempFile)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency Security
|
||||||
|
|
||||||
|
### Vulnerability Scanning
|
||||||
|
|
||||||
|
**Tools:**
|
||||||
|
- `npm audit` - Check for known vulnerabilities
|
||||||
|
- Renovate bot - Automatic dependency updates
|
||||||
|
- GitHub Dependabot alerts
|
||||||
|
|
||||||
|
**Process:**
|
||||||
|
```bash
|
||||||
|
# Check for vulnerabilities
|
||||||
|
npm audit
|
||||||
|
|
||||||
|
# Fix automatically
|
||||||
|
npm audit fix
|
||||||
|
|
||||||
|
# Manual review for breaking changes
|
||||||
|
npm audit fix --force
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependency Pinning
|
||||||
|
|
||||||
|
**package.json:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"express": "4.18.2", // Exact version
|
||||||
|
"better-sqlite3": "^9.2.2" // Compatible versions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**pnpm Overrides:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"lodash@<4.17.21": ">=4.17.21", // Force minimum version
|
||||||
|
"axios@<0.21.2": ">=0.21.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Patch Management
|
||||||
|
|
||||||
|
**pnpm Patches:**
|
||||||
|
```bash
|
||||||
|
# Create patch
|
||||||
|
pnpm patch @ckeditor/ckeditor5
|
||||||
|
|
||||||
|
# Edit files in temporary directory
|
||||||
|
# ...
|
||||||
|
|
||||||
|
# Generate patch file
|
||||||
|
pnpm patch-commit /tmp/ckeditor5-patch
|
||||||
|
|
||||||
|
# Patch applied automatically on install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Best Practices
|
||||||
|
|
||||||
|
### For Users
|
||||||
|
|
||||||
|
1. **Strong Passwords**
|
||||||
|
- Use unique password for Trilium
|
||||||
|
- Enable TOTP 2FA
|
||||||
|
- Protect password manager
|
||||||
|
|
||||||
|
2. **Protected Notes**
|
||||||
|
- Use for sensitive information
|
||||||
|
- Set reasonable session timeout
|
||||||
|
- Don't leave sessions unattended
|
||||||
|
|
||||||
|
3. **Backups**
|
||||||
|
- Regular backups to secure location
|
||||||
|
- Encrypt backup storage
|
||||||
|
- Test backup restoration
|
||||||
|
|
||||||
|
4. **Server Setup**
|
||||||
|
- Use HTTPS only
|
||||||
|
- Keep software updated
|
||||||
|
- Firewall configuration
|
||||||
|
- Use reverse proxy (nginx, Caddy)
|
||||||
|
|
||||||
|
5. **Scripts**
|
||||||
|
- Review scripts before using
|
||||||
|
- Be cautious with external scripts
|
||||||
|
- Understand script permissions
|
||||||
|
|
||||||
|
### For Developers
|
||||||
|
|
||||||
|
1. **Code Review**
|
||||||
|
- Review all security-related changes
|
||||||
|
- Test authentication/authorization changes
|
||||||
|
- Validate input sanitization
|
||||||
|
|
||||||
|
2. **Testing**
|
||||||
|
- Write security tests
|
||||||
|
- Test edge cases
|
||||||
|
- Penetration testing
|
||||||
|
|
||||||
|
3. **Dependencies**
|
||||||
|
- Regular updates
|
||||||
|
- Audit new dependencies
|
||||||
|
- Monitor security advisories
|
||||||
|
|
||||||
|
4. **Secrets**
|
||||||
|
- No secrets in source code
|
||||||
|
- Use environment variables
|
||||||
|
- Secure key generation
|
||||||
|
|
||||||
|
## Security Auditing
|
||||||
|
|
||||||
|
### Logs
|
||||||
|
|
||||||
|
**Security Events Logged:**
|
||||||
|
- Login attempts (success/failure)
|
||||||
|
- Protected session access
|
||||||
|
- Password changes
|
||||||
|
- ETAPI token usage
|
||||||
|
- Failed CSRF validations
|
||||||
|
|
||||||
|
**Log Location:**
|
||||||
|
- Desktop: Console output
|
||||||
|
- Server: Log files or stdout
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
|
||||||
|
**Metrics to Monitor:**
|
||||||
|
- Failed login attempts
|
||||||
|
- API error rates
|
||||||
|
- Unusual database changes
|
||||||
|
- Large exports/imports
|
||||||
|
|
||||||
|
## Incident Response
|
||||||
|
|
||||||
|
### Security Issue Reporting
|
||||||
|
|
||||||
|
**Process:**
|
||||||
|
1. Email security@triliumnext.com
|
||||||
|
2. Include vulnerability details
|
||||||
|
3. Provide reproduction steps
|
||||||
|
4. Allow reasonable disclosure time
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
1. Acknowledge within 48 hours
|
||||||
|
2. Investigate and validate
|
||||||
|
3. Develop fix
|
||||||
|
4. Coordinate disclosure
|
||||||
|
5. Release patch
|
||||||
|
|
||||||
|
### Breach Response
|
||||||
|
|
||||||
|
**If Compromised:**
|
||||||
|
1. Change password immediately
|
||||||
|
2. Review recent activity
|
||||||
|
3. Check for unauthorized changes
|
||||||
|
4. Restore from backup if needed
|
||||||
|
5. Update security settings
|
||||||
|
|
||||||
|
## Future Security Enhancements
|
||||||
|
|
||||||
|
**Planned:**
|
||||||
|
- Hardware security key support (U2F/WebAuthn)
|
||||||
|
- End-to-end encryption for sync
|
||||||
|
- Zero-knowledge architecture option
|
||||||
|
- Encryption key rotation
|
||||||
|
- Audit log enhancements
|
||||||
|
- Per-note access controls
|
||||||
|
|
||||||
|
**Under Consideration:**
|
||||||
|
- Multi-user support with permissions
|
||||||
|
- Blockchain-based sync verification
|
||||||
|
- Homomorphic encryption for search
|
||||||
|
- Quantum-resistant encryption
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**See Also:**
|
||||||
|
- [SECURITY.md](../SECURITY.md) - Security policy
|
||||||
|
- [ARCHITECTURE.md](ARCHITECTURE.md) - Overall architecture
|
||||||
|
- [Protected Notes Guide](https://triliumnext.github.io/Docs/Wiki/protected-notes)
|
||||||
583
docs/SYNCHRONIZATION.md
vendored
Normal file
583
docs/SYNCHRONIZATION.md
vendored
Normal file
@ -0,0 +1,583 @@
|
|||||||
|
# Trilium Synchronization Architecture
|
||||||
|
|
||||||
|
> **Related:** [ARCHITECTURE.md](ARCHITECTURE.md) | [User Guide: Synchronization](https://triliumnext.github.io/Docs/Wiki/synchronization)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Trilium implements a sophisticated **bidirectional synchronization system** that allows users to sync their note databases across multiple devices (desktop clients and server instances). The sync protocol is designed to handle:
|
||||||
|
|
||||||
|
- Concurrent modifications across devices
|
||||||
|
- Conflict resolution
|
||||||
|
- Partial sync (only changed entities)
|
||||||
|
- Protected note synchronization
|
||||||
|
- Efficient bandwidth usage
|
||||||
|
|
||||||
|
## Sync Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐ ┌─────────────┐
|
||||||
|
│ Desktop 1 │ │ Desktop 2 │
|
||||||
|
│ (Client) │ │ (Client) │
|
||||||
|
└──────┬──────┘ └──────┬──────┘
|
||||||
|
│ │
|
||||||
|
│ WebSocket/HTTP │
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌────────────────────────────────────────────────┐
|
||||||
|
│ Sync Server │
|
||||||
|
│ ┌──────────────────────────────────────┐ │
|
||||||
|
│ │ Sync Service │ │
|
||||||
|
│ │ - Entity Change Management │ │
|
||||||
|
│ │ - Conflict Resolution │ │
|
||||||
|
│ │ - Version Tracking │ │
|
||||||
|
│ └──────────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌──────┴───────┐ │
|
||||||
|
│ │ Database │ │
|
||||||
|
│ │ (entity_changes)│ │
|
||||||
|
│ └──────────────┘ │
|
||||||
|
└────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### Entity Changes
|
||||||
|
|
||||||
|
Every modification to any entity (note, branch, attribute, etc.) creates an **entity change** record:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
entity_changes (
|
||||||
|
id, -- Auto-increment ID
|
||||||
|
entityName, -- 'notes', 'branches', 'attributes', etc.
|
||||||
|
entityId, -- ID of the changed entity
|
||||||
|
hash, -- Content hash for integrity
|
||||||
|
isErased, -- If entity was erased (deleted permanently)
|
||||||
|
changeId, -- Unique change identifier
|
||||||
|
componentId, -- Installation identifier
|
||||||
|
instanceId, -- Process instance identifier
|
||||||
|
isSynced, -- Whether synced to server
|
||||||
|
utcDateChanged -- When change occurred
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Properties:**
|
||||||
|
- **changeId**: Globally unique identifier (UUID) for the change
|
||||||
|
- **componentId**: Unique per Trilium installation (persists across restarts)
|
||||||
|
- **instanceId**: Unique per process (changes on restart)
|
||||||
|
- **hash**: SHA-256 hash of entity data for integrity verification
|
||||||
|
|
||||||
|
### Sync Versions
|
||||||
|
|
||||||
|
Each Trilium installation tracks:
|
||||||
|
- **Local sync version**: Highest change ID seen locally
|
||||||
|
- **Server sync version**: Highest change ID on server
|
||||||
|
- **Entity versions**: Last sync version for each entity type
|
||||||
|
|
||||||
|
### Change Tracking
|
||||||
|
|
||||||
|
**When an entity is modified:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// apps/server/src/services/entity_changes.ts
|
||||||
|
function addEntityChange(entityName, entityId, entity) {
|
||||||
|
const hash = calculateHash(entity)
|
||||||
|
const changeId = generateUUID()
|
||||||
|
|
||||||
|
sql.insert('entity_changes', {
|
||||||
|
entityName,
|
||||||
|
entityId,
|
||||||
|
hash,
|
||||||
|
changeId,
|
||||||
|
componentId: config.componentId,
|
||||||
|
instanceId: config.instanceId,
|
||||||
|
isSynced: 0,
|
||||||
|
utcDateChanged: now()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Entity modification triggers:**
|
||||||
|
- Note content update
|
||||||
|
- Note metadata change
|
||||||
|
- Branch creation/deletion/reorder
|
||||||
|
- Attribute addition/removal
|
||||||
|
- Options modification
|
||||||
|
|
||||||
|
## Sync Protocol
|
||||||
|
|
||||||
|
### Sync Handshake
|
||||||
|
|
||||||
|
**Step 1: Client Initiates Sync**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Client sends current sync version
|
||||||
|
POST /api/sync/check
|
||||||
|
{
|
||||||
|
"sourceId": "client-component-id",
|
||||||
|
"maxChangeId": 12345
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Server Responds with Status**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Server checks for changes
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"entityChanges": 567, // Changes on server
|
||||||
|
"maxChangeId": 12890, // Server's max change ID
|
||||||
|
"outstandingPushCount": 23 // Client changes not yet synced
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 3: Decision**
|
||||||
|
|
||||||
|
- If `entityChanges > 0`: Pull changes from server
|
||||||
|
- If `outstandingPushCount > 0`: Push changes to server
|
||||||
|
- Both can happen in sequence
|
||||||
|
|
||||||
|
### Pull Sync (Server → Client)
|
||||||
|
|
||||||
|
**Client Requests Changes:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
POST /api/sync/pull
|
||||||
|
{
|
||||||
|
"sourceId": "client-component-id",
|
||||||
|
"lastSyncedChangeId": 12345
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Server Responds:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
Response:
|
||||||
|
{
|
||||||
|
"notes": [
|
||||||
|
{ noteId: "abc", title: "New Note", ... }
|
||||||
|
],
|
||||||
|
"branches": [...],
|
||||||
|
"attributes": [...],
|
||||||
|
"revisions": [...],
|
||||||
|
"attachments": [...],
|
||||||
|
"entityChanges": [
|
||||||
|
{ entityName: "notes", entityId: "abc", changeId: "...", ... }
|
||||||
|
],
|
||||||
|
"maxChangeId": 12890
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Client Processing:**
|
||||||
|
|
||||||
|
1. Apply entity changes to local database
|
||||||
|
2. Update Froca cache
|
||||||
|
3. Update local sync version
|
||||||
|
4. Trigger UI refresh
|
||||||
|
|
||||||
|
### Push Sync (Client → Server)
|
||||||
|
|
||||||
|
**Client Sends Changes:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
POST /api/sync/push
|
||||||
|
{
|
||||||
|
"sourceId": "client-component-id",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"noteId": "xyz",
|
||||||
|
"title": "Modified Note",
|
||||||
|
...
|
||||||
|
},
|
||||||
|
"entityChange": {
|
||||||
|
"changeId": "change-uuid",
|
||||||
|
"entityName": "notes",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Server Processing:**
|
||||||
|
|
||||||
|
1. Validate changes
|
||||||
|
2. Check for conflicts
|
||||||
|
3. Apply changes to database
|
||||||
|
4. Update Becca cache
|
||||||
|
5. Mark as synced
|
||||||
|
6. Broadcast to other connected clients via WebSocket
|
||||||
|
|
||||||
|
**Conflict Detection:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Check if entity was modified on server since client's last sync
|
||||||
|
const serverEntity = becca.getNote(noteId)
|
||||||
|
const serverLastModified = serverEntity.utcDateModified
|
||||||
|
|
||||||
|
if (serverLastModified > clientSyncVersion) {
|
||||||
|
// CONFLICT!
|
||||||
|
resolveConflict(serverEntity, clientEntity)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conflict Resolution
|
||||||
|
|
||||||
|
### Conflict Types
|
||||||
|
|
||||||
|
**1. Content Conflict**
|
||||||
|
- Both client and server modified same note content
|
||||||
|
- **Resolution**: Last-write-wins based on `utcDateModified`
|
||||||
|
|
||||||
|
**2. Structure Conflict**
|
||||||
|
- Branch moved/deleted on one side, modified on other
|
||||||
|
- **Resolution**: Tombstone records, reconciliation
|
||||||
|
|
||||||
|
**3. Attribute Conflict**
|
||||||
|
- Same attribute modified differently
|
||||||
|
- **Resolution**: Last-write-wins
|
||||||
|
|
||||||
|
### Conflict Resolution Strategy
|
||||||
|
|
||||||
|
**Last-Write-Wins:**
|
||||||
|
```typescript
|
||||||
|
if (clientEntity.utcDateModified > serverEntity.utcDateModified) {
|
||||||
|
// Client wins, apply client changes
|
||||||
|
applyClientChange(clientEntity)
|
||||||
|
} else {
|
||||||
|
// Server wins, reject client change
|
||||||
|
// Client will pull server version on next sync
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Tombstone Records:**
|
||||||
|
- Deleted entities leave tombstone in `entity_changes`
|
||||||
|
- Prevents re-sync of deleted items
|
||||||
|
- `isErased = 1` for permanent deletions
|
||||||
|
|
||||||
|
### Protected Notes Sync
|
||||||
|
|
||||||
|
**Challenge:** Encrypted content can't be synced without password
|
||||||
|
|
||||||
|
**Solution:**
|
||||||
|
|
||||||
|
1. **Protected session required**: User must unlock protected notes
|
||||||
|
2. **Encrypted sync**: Content synced in encrypted form
|
||||||
|
3. **Hash verification**: Integrity checked without decryption
|
||||||
|
4. **Lazy decryption**: Only decrypt when accessed
|
||||||
|
|
||||||
|
**Sync Flow:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Client side
|
||||||
|
if (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) {
|
||||||
|
// Skip protected notes if session not active
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server side
|
||||||
|
if (note.isProtected) {
|
||||||
|
// Sync encrypted blob
|
||||||
|
// Don't decrypt for sync
|
||||||
|
syncEncryptedBlob(note.blobId)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sync States
|
||||||
|
|
||||||
|
### Connection States
|
||||||
|
|
||||||
|
- **Connected**: WebSocket connection active
|
||||||
|
- **Disconnected**: No connection to sync server
|
||||||
|
- **Syncing**: Actively transferring data
|
||||||
|
- **Conflict**: Sync paused due to conflict
|
||||||
|
|
||||||
|
### Entity Sync States
|
||||||
|
|
||||||
|
Each entity can be in:
|
||||||
|
- **Synced**: In sync with server
|
||||||
|
- **Pending**: Local changes not yet pushed
|
||||||
|
- **Conflict**: Conflicting changes detected
|
||||||
|
|
||||||
|
### UI Indicators
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// apps/client/src/widgets/sync_status.ts
|
||||||
|
class SyncStatusWidget {
|
||||||
|
showSyncStatus() {
|
||||||
|
if (isConnected && allSynced) {
|
||||||
|
showIcon('synced')
|
||||||
|
} else if (isSyncing) {
|
||||||
|
showIcon('syncing-spinner')
|
||||||
|
} else if (hasConflicts) {
|
||||||
|
showIcon('conflict-warning')
|
||||||
|
} else {
|
||||||
|
showIcon('not-synced')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Optimizations
|
||||||
|
|
||||||
|
### Incremental Sync
|
||||||
|
|
||||||
|
Only entities changed since last sync are transferred:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT * FROM entity_changes
|
||||||
|
WHERE id > :lastSyncedChangeId
|
||||||
|
ORDER BY id ASC
|
||||||
|
LIMIT 1000
|
||||||
|
```
|
||||||
|
|
||||||
|
### Batch Processing
|
||||||
|
|
||||||
|
Changes sent in batches to reduce round trips:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const BATCH_SIZE = 1000
|
||||||
|
const changes = getUnsyncedChanges(BATCH_SIZE)
|
||||||
|
await syncBatch(changes)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hash-Based Change Detection
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Only sync if hash differs
|
||||||
|
const localHash = calculateHash(localEntity)
|
||||||
|
const serverHash = getServerHash(entityId)
|
||||||
|
|
||||||
|
if (localHash !== serverHash) {
|
||||||
|
syncEntity(localEntity)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compression
|
||||||
|
|
||||||
|
Large payloads compressed before transmission:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Server sends compressed response
|
||||||
|
res.setHeader('Content-Encoding', 'gzip')
|
||||||
|
res.send(gzip(syncData))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Network Errors
|
||||||
|
|
||||||
|
**Retry Strategy:**
|
||||||
|
```typescript
|
||||||
|
const RETRY_DELAYS = [1000, 2000, 5000, 10000, 30000]
|
||||||
|
|
||||||
|
async function syncWithRetry(attempt = 0) {
|
||||||
|
try {
|
||||||
|
await performSync()
|
||||||
|
} catch (error) {
|
||||||
|
if (attempt < RETRY_DELAYS.length) {
|
||||||
|
setTimeout(() => {
|
||||||
|
syncWithRetry(attempt + 1)
|
||||||
|
}, RETRY_DELAYS[attempt])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sync Integrity Checks
|
||||||
|
|
||||||
|
**Hash Verification:**
|
||||||
|
```typescript
|
||||||
|
// Verify entity hash matches
|
||||||
|
const calculatedHash = calculateHash(entity)
|
||||||
|
const receivedHash = entityChange.hash
|
||||||
|
|
||||||
|
if (calculatedHash !== receivedHash) {
|
||||||
|
throw new Error('Hash mismatch - data corruption detected')
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Consistency Checks:**
|
||||||
|
- Orphaned branches detection
|
||||||
|
- Missing parent notes
|
||||||
|
- Invalid entity references
|
||||||
|
- Circular dependencies
|
||||||
|
|
||||||
|
## Sync Server Configuration
|
||||||
|
|
||||||
|
### Server Setup
|
||||||
|
|
||||||
|
**Required Options:**
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
"syncServerHost": "https://sync.example.com",
|
||||||
|
"syncServerTimeout": 60000,
|
||||||
|
"syncProxy": "" // Optional HTTP proxy
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Authentication:**
|
||||||
|
- Username/password or
|
||||||
|
- Sync token (generated on server)
|
||||||
|
|
||||||
|
### Client Setup
|
||||||
|
|
||||||
|
**Desktop Client:**
|
||||||
|
```javascript
|
||||||
|
// Settings → Sync
|
||||||
|
{
|
||||||
|
"syncServerHost": "https://sync.example.com",
|
||||||
|
"username": "user@example.com",
|
||||||
|
"password": "********"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Connection:**
|
||||||
|
```typescript
|
||||||
|
POST /api/sync/test
|
||||||
|
Response: { "success": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sync API Endpoints
|
||||||
|
|
||||||
|
Located at: `apps/server/src/routes/api/sync.ts`
|
||||||
|
|
||||||
|
**Endpoints:**
|
||||||
|
|
||||||
|
- `POST /api/sync/check` - Check sync status
|
||||||
|
- `POST /api/sync/pull` - Pull changes from server
|
||||||
|
- `POST /api/sync/push` - Push changes to server
|
||||||
|
- `POST /api/sync/finished` - Mark sync complete
|
||||||
|
- `POST /api/sync/test` - Test connection
|
||||||
|
- `GET /api/sync/stats` - Sync statistics
|
||||||
|
|
||||||
|
## WebSocket Sync Updates
|
||||||
|
|
||||||
|
Real-time sync via WebSocket:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Server broadcasts change to all connected clients
|
||||||
|
ws.broadcast('entity-change', {
|
||||||
|
entityName: 'notes',
|
||||||
|
entityId: 'abc123',
|
||||||
|
changeId: 'change-uuid',
|
||||||
|
sourceId: 'originating-component-id'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Client receives and applies
|
||||||
|
ws.on('entity-change', (data) => {
|
||||||
|
if (data.sourceId !== myComponentId) {
|
||||||
|
froca.processEntityChange(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sync Scheduling
|
||||||
|
|
||||||
|
### Automatic Sync
|
||||||
|
|
||||||
|
**Desktop:**
|
||||||
|
- Sync on startup
|
||||||
|
- Periodic sync (configurable interval, default: 60s)
|
||||||
|
- Sync before shutdown
|
||||||
|
|
||||||
|
**Server:**
|
||||||
|
- Sync on entity modification
|
||||||
|
- WebSocket push to connected clients
|
||||||
|
|
||||||
|
### Manual Sync
|
||||||
|
|
||||||
|
User can trigger:
|
||||||
|
- Full sync
|
||||||
|
- Sync now
|
||||||
|
- Sync specific subtree
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Sync stuck:**
|
||||||
|
```sql
|
||||||
|
-- Reset sync state
|
||||||
|
UPDATE entity_changes SET isSynced = 0;
|
||||||
|
DELETE FROM options WHERE name LIKE 'sync%';
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hash mismatch:**
|
||||||
|
- Data corruption detected
|
||||||
|
- Re-sync from backup
|
||||||
|
- Check database integrity
|
||||||
|
|
||||||
|
**Conflict loop:**
|
||||||
|
- Manual intervention required
|
||||||
|
- Export conflicting notes
|
||||||
|
- Choose winning version
|
||||||
|
- Re-sync
|
||||||
|
|
||||||
|
### Sync Diagnostics
|
||||||
|
|
||||||
|
**Check sync status:**
|
||||||
|
```typescript
|
||||||
|
GET /api/sync/stats
|
||||||
|
Response: {
|
||||||
|
"unsyncedChanges": 0,
|
||||||
|
"lastSyncDate": "2025-11-02T12:00:00Z",
|
||||||
|
"syncVersion": 12890
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Entity change log:**
|
||||||
|
```sql
|
||||||
|
SELECT * FROM entity_changes
|
||||||
|
WHERE isSynced = 0
|
||||||
|
ORDER BY id DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Encrypted Sync
|
||||||
|
|
||||||
|
- Protected notes synced encrypted
|
||||||
|
- No plain text over network
|
||||||
|
- Server cannot read protected content
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
|
||||||
|
- Username/password over HTTPS only
|
||||||
|
- Sync tokens for token-based auth
|
||||||
|
- Session cookies with CSRF protection
|
||||||
|
|
||||||
|
### Authorization
|
||||||
|
|
||||||
|
- Users can only sync their own data
|
||||||
|
- No cross-user sync support
|
||||||
|
- Sync server validates ownership
|
||||||
|
|
||||||
|
## Performance Metrics
|
||||||
|
|
||||||
|
**Typical Sync Performance:**
|
||||||
|
- 1000 changes: ~2-5 seconds
|
||||||
|
- 10000 changes: ~20-50 seconds
|
||||||
|
- Initial full sync (100k notes): ~5-10 minutes
|
||||||
|
|
||||||
|
**Factors:**
|
||||||
|
- Network latency
|
||||||
|
- Database size
|
||||||
|
- Number of protected notes
|
||||||
|
- Attachment sizes
|
||||||
|
|
||||||
|
## Future Improvements
|
||||||
|
|
||||||
|
**Planned Enhancements:**
|
||||||
|
- Differential sync (binary diff)
|
||||||
|
- Peer-to-peer sync (no central server)
|
||||||
|
- Multi-server sync
|
||||||
|
- Partial sync (subtree only)
|
||||||
|
- Sync over Tor/I2P
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**See Also:**
|
||||||
|
- [ARCHITECTURE.md](ARCHITECTURE.md) - Overall architecture
|
||||||
|
- [Sync User Guide](https://triliumnext.github.io/Docs/Wiki/synchronization)
|
||||||
|
- [Sync API Source](../apps/server/src/routes/api/sync.ts)
|
||||||
423
docs/TECHNICAL_DOCUMENTATION.md
vendored
Normal file
423
docs/TECHNICAL_DOCUMENTATION.md
vendored
Normal file
@ -0,0 +1,423 @@
|
|||||||
|
# Trilium Notes - Technical Documentation Index
|
||||||
|
|
||||||
|
Welcome to the comprehensive technical and architectural documentation for Trilium Notes. This index provides quick access to all technical documentation resources.
|
||||||
|
|
||||||
|
## 📚 Core Architecture Documentation
|
||||||
|
|
||||||
|
### [ARCHITECTURE.md](ARCHITECTURE.md)
|
||||||
|
**Main technical architecture document** covering the complete system design.
|
||||||
|
|
||||||
|
**Topics Covered:**
|
||||||
|
- High-level architecture overview
|
||||||
|
- Monorepo structure and organization
|
||||||
|
- Core architecture patterns (Becca, Froca, Shaca)
|
||||||
|
- Entity system and data model
|
||||||
|
- Widget-based UI architecture
|
||||||
|
- Frontend and backend architecture
|
||||||
|
- API architecture (Internal, ETAPI, WebSocket)
|
||||||
|
- Build system and tooling
|
||||||
|
- Testing strategy
|
||||||
|
- Security overview
|
||||||
|
|
||||||
|
**Audience:** Developers, architects, contributors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [DATABASE.md](DATABASE.md)
|
||||||
|
**Complete database architecture and schema documentation.**
|
||||||
|
|
||||||
|
**Topics Covered:**
|
||||||
|
- SQLite database structure
|
||||||
|
- Entity tables (notes, branches, attributes, revisions, attachments, blobs)
|
||||||
|
- System tables (options, entity_changes, sessions)
|
||||||
|
- Data relationships and integrity
|
||||||
|
- Database access patterns
|
||||||
|
- Migrations and versioning
|
||||||
|
- Performance optimization
|
||||||
|
- Backup and maintenance
|
||||||
|
- Security considerations
|
||||||
|
|
||||||
|
**Audience:** Backend developers, database administrators
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [SYNCHRONIZATION.md](SYNCHRONIZATION.md)
|
||||||
|
**Detailed synchronization protocol and implementation.**
|
||||||
|
|
||||||
|
**Topics Covered:**
|
||||||
|
- Sync architecture overview
|
||||||
|
- Entity change tracking
|
||||||
|
- Sync protocol (handshake, pull, push)
|
||||||
|
- Conflict resolution strategies
|
||||||
|
- Protected notes synchronization
|
||||||
|
- Performance optimizations
|
||||||
|
- Error handling and retry logic
|
||||||
|
- Sync server configuration
|
||||||
|
- WebSocket real-time updates
|
||||||
|
- Troubleshooting guide
|
||||||
|
|
||||||
|
**Audience:** Advanced users, sync server administrators, contributors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [SCRIPTING.md](SCRIPTING.md)
|
||||||
|
**Comprehensive guide to the Trilium scripting system.**
|
||||||
|
|
||||||
|
**Topics Covered:**
|
||||||
|
- Script types (frontend, backend, render)
|
||||||
|
- Frontend API reference
|
||||||
|
- Backend API reference
|
||||||
|
- Entity classes (FNote, BNote, etc.)
|
||||||
|
- Script examples and patterns
|
||||||
|
- Script storage and execution
|
||||||
|
- Security considerations
|
||||||
|
- Performance optimization
|
||||||
|
- Debugging techniques
|
||||||
|
- Advanced topics
|
||||||
|
|
||||||
|
**Audience:** Power users, script developers, plugin creators
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [SECURITY_ARCHITECTURE.md](SECURITY_ARCHITECTURE.md)
|
||||||
|
**In-depth security architecture and implementation.**
|
||||||
|
|
||||||
|
**Topics Covered:**
|
||||||
|
- Security principles and threat model
|
||||||
|
- Authentication methods (password, TOTP, OpenID)
|
||||||
|
- Session management
|
||||||
|
- Authorization and protected sessions
|
||||||
|
- Encryption (notes, transport, backups)
|
||||||
|
- Input sanitization (XSS, SQL injection, CSRF)
|
||||||
|
- Network security (HTTPS, headers, rate limiting)
|
||||||
|
- Data security and secure deletion
|
||||||
|
- Dependency security
|
||||||
|
- Security best practices
|
||||||
|
- Incident response
|
||||||
|
|
||||||
|
**Audience:** Security engineers, administrators, auditors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Developer Documentation
|
||||||
|
|
||||||
|
### [Developer Guide](Developer%20Guide/Developer%20Guide/)
|
||||||
|
Collection of developer-focused documentation for contributing to Trilium.
|
||||||
|
|
||||||
|
**Key Documents:**
|
||||||
|
- [Environment Setup](Developer%20Guide/Developer%20Guide/Environment%20Setup.md) - Setting up development environment
|
||||||
|
- [Project Structure](Developer%20Guide/Developer%20Guide/Project%20Structure.md) - Monorepo organization
|
||||||
|
- [Development and Architecture](Developer%20Guide/Developer%20Guide/Development%20and%20architecture/) - Various development topics
|
||||||
|
|
||||||
|
**Topics Include:**
|
||||||
|
- Local development setup
|
||||||
|
- Building and deployment
|
||||||
|
- Adding new note types
|
||||||
|
- Database schema details
|
||||||
|
- Internationalization
|
||||||
|
- Icons and UI customization
|
||||||
|
- Docker development
|
||||||
|
- Troubleshooting
|
||||||
|
|
||||||
|
**Audience:** Contributors, developers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 User Documentation
|
||||||
|
|
||||||
|
### [User Guide](User%20Guide/User%20Guide/)
|
||||||
|
Comprehensive end-user documentation for using Trilium.
|
||||||
|
|
||||||
|
**Key Sections:**
|
||||||
|
- Installation & Setup
|
||||||
|
- Basic Concepts and Features
|
||||||
|
- Note Types
|
||||||
|
- Advanced Usage
|
||||||
|
- Synchronization
|
||||||
|
- Import/Export
|
||||||
|
|
||||||
|
**Audience:** End users, administrators
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### [Script API](Script%20API/)
|
||||||
|
Complete API reference for user scripting.
|
||||||
|
|
||||||
|
**Coverage:**
|
||||||
|
- Frontend API methods
|
||||||
|
- Backend API methods
|
||||||
|
- Entity properties and methods
|
||||||
|
- Event handlers
|
||||||
|
- Utility functions
|
||||||
|
|
||||||
|
**Audience:** Script developers, power users
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Quick Start Guides
|
||||||
|
|
||||||
|
### For Users
|
||||||
|
1. [Installation Guide](User%20Guide/User%20Guide/Installation%20&%20Setup/) - Get Trilium running
|
||||||
|
2. [Basic Concepts](User%20Guide/User%20Guide/Basic%20Concepts%20and%20Features/) - Learn the fundamentals
|
||||||
|
3. [Scripting Guide](SCRIPTING.md) - Extend Trilium with scripts
|
||||||
|
|
||||||
|
### For Developers
|
||||||
|
1. [Environment Setup](Developer%20Guide/Developer%20Guide/Environment%20Setup.md) - Setup development environment
|
||||||
|
2. [Architecture Overview](ARCHITECTURE.md) - Understand the system
|
||||||
|
3. [Contributing Guide](../README.md#-contribute) - Start contributing
|
||||||
|
|
||||||
|
### For Administrators
|
||||||
|
1. [Server Installation](User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation.md) - Deploy Trilium server
|
||||||
|
2. [Synchronization Setup](SYNCHRONIZATION.md) - Configure sync
|
||||||
|
3. [Security Best Practices](SECURITY_ARCHITECTURE.md#security-best-practices) - Secure your installation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔍 Documentation by Topic
|
||||||
|
|
||||||
|
### Architecture & Design
|
||||||
|
- [Overall Architecture](ARCHITECTURE.md)
|
||||||
|
- [Monorepo Structure](ARCHITECTURE.md#monorepo-structure)
|
||||||
|
- [Three-Layer Cache System](ARCHITECTURE.md#three-layer-cache-system)
|
||||||
|
- [Entity System](ARCHITECTURE.md#entity-system)
|
||||||
|
- [Widget-Based UI](ARCHITECTURE.md#widget-based-ui)
|
||||||
|
|
||||||
|
### Data & Storage
|
||||||
|
- [Database Architecture](DATABASE.md)
|
||||||
|
- [Entity Tables](DATABASE.md#entity-tables)
|
||||||
|
- [Data Relationships](DATABASE.md#data-relationships)
|
||||||
|
- [Blob Storage](DATABASE.md#blobs-table)
|
||||||
|
- [Database Migrations](DATABASE.md#database-migrations)
|
||||||
|
|
||||||
|
### Synchronization
|
||||||
|
- [Sync Architecture](SYNCHRONIZATION.md#sync-architecture)
|
||||||
|
- [Sync Protocol](SYNCHRONIZATION.md#sync-protocol)
|
||||||
|
- [Conflict Resolution](SYNCHRONIZATION.md#conflict-resolution)
|
||||||
|
- [Protected Notes Sync](SYNCHRONIZATION.md#protected-notes-sync)
|
||||||
|
- [WebSocket Sync](SYNCHRONIZATION.md#websocket-sync-updates)
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- [Authentication](SECURITY_ARCHITECTURE.md#authentication)
|
||||||
|
- [Encryption](SECURITY_ARCHITECTURE.md#encryption)
|
||||||
|
- [Input Sanitization](SECURITY_ARCHITECTURE.md#input-sanitization)
|
||||||
|
- [Network Security](SECURITY_ARCHITECTURE.md#network-security)
|
||||||
|
- [Security Best Practices](SECURITY_ARCHITECTURE.md#security-best-practices)
|
||||||
|
|
||||||
|
### Scripting & Extensibility
|
||||||
|
- [Script Types](SCRIPTING.md#script-types)
|
||||||
|
- [Frontend API](SCRIPTING.md#frontend-api)
|
||||||
|
- [Backend API](SCRIPTING.md#backend-api)
|
||||||
|
- [Script Examples](SCRIPTING.md#script-examples)
|
||||||
|
- [Custom Widgets](SCRIPTING.md#render-scripts)
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
- [Client Architecture](ARCHITECTURE.md#frontend-architecture)
|
||||||
|
- [Widget System](ARCHITECTURE.md#widget-based-ui)
|
||||||
|
- [Event System](ARCHITECTURE.md#event-system)
|
||||||
|
- [Froca Cache](ARCHITECTURE.md#2-froca-frontend-cache)
|
||||||
|
- [UI Components](ARCHITECTURE.md#ui-components)
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
- [Server Architecture](ARCHITECTURE.md#backend-architecture)
|
||||||
|
- [Service Layer](ARCHITECTURE.md#service-layer)
|
||||||
|
- [Route Structure](ARCHITECTURE.md#route-structure)
|
||||||
|
- [Becca Cache](ARCHITECTURE.md#1-becca-backend-cache)
|
||||||
|
- [Middleware](ARCHITECTURE.md#middleware)
|
||||||
|
|
||||||
|
### Build & Deploy
|
||||||
|
- [Build System](ARCHITECTURE.md#build-system)
|
||||||
|
- [Package Manager](ARCHITECTURE.md#package-manager-pnpm)
|
||||||
|
- [Build Tools](ARCHITECTURE.md#build-tools)
|
||||||
|
- [Docker](Developer%20Guide/Developer%20Guide/Development%20and%20architecture/Docker.md)
|
||||||
|
- [Deployment](Developer%20Guide/Developer%20Guide/Building%20and%20deployment/)
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [Testing Strategy](ARCHITECTURE.md#testing-strategy)
|
||||||
|
- [Test Organization](ARCHITECTURE.md#test-organization)
|
||||||
|
- [E2E Testing](ARCHITECTURE.md#e2e-testing)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Reference Documentation
|
||||||
|
|
||||||
|
### File Locations
|
||||||
|
```
|
||||||
|
trilium/
|
||||||
|
├── apps/
|
||||||
|
│ ├── client/ # Frontend application
|
||||||
|
│ ├── server/ # Backend server
|
||||||
|
│ ├── desktop/ # Electron app
|
||||||
|
│ └── ...
|
||||||
|
├── packages/
|
||||||
|
│ ├── commons/ # Shared code
|
||||||
|
│ ├── ckeditor5/ # Rich text editor
|
||||||
|
│ └── ...
|
||||||
|
├── docs/
|
||||||
|
│ ├── ARCHITECTURE.md # Main architecture doc
|
||||||
|
│ ├── DATABASE.md # Database documentation
|
||||||
|
│ ├── SYNCHRONIZATION.md # Sync documentation
|
||||||
|
│ ├── SCRIPTING.md # Scripting guide
|
||||||
|
│ ├── SECURITY_ARCHITECTURE.md # Security documentation
|
||||||
|
│ ├── Developer Guide/ # Developer docs
|
||||||
|
│ ├── User Guide/ # User docs
|
||||||
|
│ └── Script API/ # API reference
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Source Files
|
||||||
|
- **Backend Entry:** `apps/server/src/main.ts`
|
||||||
|
- **Frontend Entry:** `apps/client/src/desktop.ts` / `apps/client/src/index.ts`
|
||||||
|
- **Becca Cache:** `apps/server/src/becca/becca.ts`
|
||||||
|
- **Froca Cache:** `apps/client/src/services/froca.ts`
|
||||||
|
- **Database Schema:** `apps/server/src/assets/db/schema.sql`
|
||||||
|
- **Backend API:** `apps/server/src/services/backend_script_api.ts`
|
||||||
|
- **Frontend API:** `apps/client/src/services/frontend_script_api.ts`
|
||||||
|
|
||||||
|
### Important Directories
|
||||||
|
- **Entities:** `apps/server/src/becca/entities/`
|
||||||
|
- **Widgets:** `apps/client/src/widgets/`
|
||||||
|
- **Services:** `apps/server/src/services/`
|
||||||
|
- **Routes:** `apps/server/src/routes/`
|
||||||
|
- **Migrations:** `apps/server/src/migrations/`
|
||||||
|
- **Tests:** Various `*.spec.ts` files throughout
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Common Tasks
|
||||||
|
|
||||||
|
### Understanding the Codebase
|
||||||
|
1. Read [ARCHITECTURE.md](ARCHITECTURE.md) for overview
|
||||||
|
2. Explore [Monorepo Structure](ARCHITECTURE.md#monorepo-structure)
|
||||||
|
3. Review [Entity System](ARCHITECTURE.md#entity-system)
|
||||||
|
4. Check [Key Files](ARCHITECTURE.md#key-files-for-understanding-architecture)
|
||||||
|
|
||||||
|
### Adding Features
|
||||||
|
1. Review relevant architecture documentation
|
||||||
|
2. Check [Developer Guide](Developer%20Guide/Developer%20Guide/)
|
||||||
|
3. Follow existing patterns in codebase
|
||||||
|
4. Write tests
|
||||||
|
5. Update documentation
|
||||||
|
|
||||||
|
### Debugging Issues
|
||||||
|
1. Check [Troubleshooting](Developer%20Guide/Developer%20Guide/Troubleshooting/)
|
||||||
|
2. Review [Database](DATABASE.md) for data issues
|
||||||
|
3. Check [Synchronization](SYNCHRONIZATION.md) for sync issues
|
||||||
|
4. Review [Security](SECURITY_ARCHITECTURE.md) for auth issues
|
||||||
|
|
||||||
|
### Performance Optimization
|
||||||
|
1. [Database Performance](DATABASE.md#performance-optimization)
|
||||||
|
2. [Cache Optimization](ARCHITECTURE.md#caching-system)
|
||||||
|
3. [Build Optimization](ARCHITECTURE.md#build-system)
|
||||||
|
4. [Script Performance](SCRIPTING.md#performance-considerations)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 External Resources
|
||||||
|
|
||||||
|
### Official Links
|
||||||
|
- **Website:** https://triliumnotes.org
|
||||||
|
- **Documentation:** https://docs.triliumnotes.org
|
||||||
|
- **GitHub:** https://github.com/TriliumNext/Trilium
|
||||||
|
- **Discussions:** https://github.com/TriliumNext/Trilium/discussions
|
||||||
|
- **Matrix Chat:** https://matrix.to/#/#triliumnext:matrix.org
|
||||||
|
|
||||||
|
### Community Resources
|
||||||
|
- **Awesome Trilium:** https://github.com/Nriver/awesome-trilium
|
||||||
|
- **TriliumRocks:** https://trilium.rocks/
|
||||||
|
- **Wiki:** https://triliumnext.github.io/Docs/Wiki/
|
||||||
|
|
||||||
|
### Related Projects
|
||||||
|
- **TriliumDroid:** https://github.com/FliegendeWurst/TriliumDroid
|
||||||
|
- **Web Clipper:** Included in main repository
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Documentation Conventions
|
||||||
|
|
||||||
|
### Document Structure
|
||||||
|
- Overview section
|
||||||
|
- Table of contents
|
||||||
|
- Main content with headings
|
||||||
|
- Code examples where relevant
|
||||||
|
- "See Also" references
|
||||||
|
|
||||||
|
### Code Examples
|
||||||
|
```typescript
|
||||||
|
// TypeScript examples with comments
|
||||||
|
const example = 'value'
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- SQL examples with formatting
|
||||||
|
SELECT * FROM notes WHERE noteId = ?
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cross-References
|
||||||
|
- Use relative links: `[text](path/to/file.md)`
|
||||||
|
- Reference sections: `[text](file.md#section)`
|
||||||
|
- External links: Full URLs
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
- Review on major releases
|
||||||
|
- Update for architectural changes
|
||||||
|
- Add examples for new features
|
||||||
|
- Keep API references current
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 Contributing to Documentation
|
||||||
|
|
||||||
|
### What to Document
|
||||||
|
- New features and APIs
|
||||||
|
- Architecture changes
|
||||||
|
- Migration guides
|
||||||
|
- Performance tips
|
||||||
|
- Security considerations
|
||||||
|
|
||||||
|
### How to Contribute
|
||||||
|
1. Edit markdown files in `docs/`
|
||||||
|
2. Follow existing structure and style
|
||||||
|
3. Include code examples
|
||||||
|
4. Test links and formatting
|
||||||
|
5. Submit pull request
|
||||||
|
|
||||||
|
### Documentation Standards
|
||||||
|
- Clear, concise language
|
||||||
|
- Complete code examples
|
||||||
|
- Proper markdown formatting
|
||||||
|
- Cross-references to related docs
|
||||||
|
- Updated version numbers
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 Version Information
|
||||||
|
|
||||||
|
- **Documentation Version:** 0.99.3
|
||||||
|
- **Last Updated:** November 2025
|
||||||
|
- **Trilium Version:** 0.99.3+
|
||||||
|
- **Next Review:** When major architectural changes occur
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Getting Help
|
||||||
|
|
||||||
|
### For Users
|
||||||
|
- [User Guide](User%20Guide/User%20Guide/)
|
||||||
|
- [GitHub Discussions](https://github.com/TriliumNext/Trilium/discussions)
|
||||||
|
- [Matrix Chat](https://matrix.to/#/#triliumnext:matrix.org)
|
||||||
|
|
||||||
|
### For Developers
|
||||||
|
- [Developer Guide](Developer%20Guide/Developer%20Guide/)
|
||||||
|
- [Architecture Docs](ARCHITECTURE.md)
|
||||||
|
- [GitHub Issues](https://github.com/TriliumNext/Trilium/issues)
|
||||||
|
|
||||||
|
### For Contributors
|
||||||
|
- [Contributing Guidelines](../README.md#-contribute)
|
||||||
|
- [Code of Conduct](../CODE_OF_CONDUCT)
|
||||||
|
- [Developer Setup](Developer%20Guide/Developer%20Guide/Environment%20Setup.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Maintained by:** TriliumNext Team
|
||||||
|
**License:** AGPL-3.0-only
|
||||||
|
**Repository:** https://github.com/TriliumNext/Trilium
|
||||||
Loading…
x
Reference in New Issue
Block a user