mirror of
https://github.com/zadam/trilium.git
synced 2025-11-03 20:06:08 +01:00
Compare commits
3 Commits
copilot/im
...
copilot/ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07fe42d04e | ||
|
|
154492e454 | ||
|
|
3e0d1bfa44 |
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/)
|
||||
155
docs/QUICK_REFERENCE.md
vendored
Normal file
155
docs/QUICK_REFERENCE.md
vendored
Normal file
@@ -0,0 +1,155 @@
|
||||
# Trilium Technical Documentation - Quick Reference
|
||||
|
||||
> **Start here:** [TECHNICAL_DOCUMENTATION.md](TECHNICAL_DOCUMENTATION.md) - Complete index of all documentation
|
||||
|
||||
## 📖 Documentation Files
|
||||
|
||||
| Document | Description | Size | Lines |
|
||||
|----------|-------------|------|-------|
|
||||
| [TECHNICAL_DOCUMENTATION.md](TECHNICAL_DOCUMENTATION.md) | Main index and navigation hub | 13KB | 423 |
|
||||
| [ARCHITECTURE.md](ARCHITECTURE.md) | Complete system architecture | 30KB | 1,016 |
|
||||
| [DATABASE.md](DATABASE.md) | Database schema and operations | 19KB | 736 |
|
||||
| [SYNCHRONIZATION.md](SYNCHRONIZATION.md) | Sync protocol and implementation | 14KB | 583 |
|
||||
| [SCRIPTING.md](SCRIPTING.md) | User scripting system guide | 17KB | 734 |
|
||||
| [SECURITY_ARCHITECTURE.md](SECURITY_ARCHITECTURE.md) | Security implementation details | 19KB | 834 |
|
||||
|
||||
**Total:** 112KB of comprehensive documentation across 4,326 lines!
|
||||
|
||||
## 🎯 Quick Access by Role
|
||||
|
||||
### 👤 End Users
|
||||
- **Getting Started:** [User Guide](User%20Guide/User%20Guide/)
|
||||
- **Scripting:** [SCRIPTING.md](SCRIPTING.md)
|
||||
- **Sync Setup:** [SYNCHRONIZATION.md](SYNCHRONIZATION.md)
|
||||
|
||||
### 💻 Developers
|
||||
- **Architecture:** [ARCHITECTURE.md](ARCHITECTURE.md)
|
||||
- **Development Setup:** [Developer Guide](Developer%20Guide/Developer%20Guide/Environment%20Setup.md)
|
||||
- **Database:** [DATABASE.md](DATABASE.md)
|
||||
|
||||
### 🔒 Security Auditors
|
||||
- **Security:** [SECURITY_ARCHITECTURE.md](SECURITY_ARCHITECTURE.md)
|
||||
- **Encryption:** [SECURITY_ARCHITECTURE.md#encryption](SECURITY_ARCHITECTURE.md#encryption)
|
||||
- **Auth:** [SECURITY_ARCHITECTURE.md#authentication](SECURITY_ARCHITECTURE.md#authentication)
|
||||
|
||||
### 🏗️ System Architects
|
||||
- **Overall Design:** [ARCHITECTURE.md](ARCHITECTURE.md)
|
||||
- **Cache System:** [ARCHITECTURE.md#three-layer-cache-system](ARCHITECTURE.md#three-layer-cache-system)
|
||||
- **Entity Model:** [ARCHITECTURE.md#entity-system](ARCHITECTURE.md#entity-system)
|
||||
|
||||
### 🔧 DevOps Engineers
|
||||
- **Server Installation:** [User Guide - Server Installation](User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation.md)
|
||||
- **Docker:** [Developer Guide - Docker](Developer%20Guide/Developer%20Guide/Development%20and%20architecture/Docker.md)
|
||||
- **Sync Server:** [SYNCHRONIZATION.md#sync-server-configuration](SYNCHRONIZATION.md#sync-server-configuration)
|
||||
|
||||
### 📊 Database Administrators
|
||||
- **Schema:** [DATABASE.md#database-schema](DATABASE.md#database-schema)
|
||||
- **Maintenance:** [DATABASE.md#database-maintenance](DATABASE.md#database-maintenance)
|
||||
- **Performance:** [DATABASE.md#performance-optimization](DATABASE.md#performance-optimization)
|
||||
|
||||
## 🔍 Quick Topic Finder
|
||||
|
||||
### Core Concepts
|
||||
- **Becca Cache:** [ARCHITECTURE.md#1-becca-backend-cache](ARCHITECTURE.md#1-becca-backend-cache)
|
||||
- **Froca Cache:** [ARCHITECTURE.md#2-froca-frontend-cache](ARCHITECTURE.md#2-froca-frontend-cache)
|
||||
- **Entity System:** [ARCHITECTURE.md#entity-system](ARCHITECTURE.md#entity-system)
|
||||
- **Widget System:** [ARCHITECTURE.md#widget-based-ui](ARCHITECTURE.md#widget-based-ui)
|
||||
|
||||
### Database
|
||||
- **Schema Overview:** [DATABASE.md#schema-overview](DATABASE.md#schema-overview)
|
||||
- **Notes Table:** [DATABASE.md#notes-table](DATABASE.md#notes-table)
|
||||
- **Branches Table:** [DATABASE.md#branches-table](DATABASE.md#branches-table)
|
||||
- **Migrations:** [DATABASE.md#database-migrations](DATABASE.md#database-migrations)
|
||||
|
||||
### Synchronization
|
||||
- **Sync Protocol:** [SYNCHRONIZATION.md#sync-protocol](SYNCHRONIZATION.md#sync-protocol)
|
||||
- **Conflict Resolution:** [SYNCHRONIZATION.md#conflict-resolution](SYNCHRONIZATION.md#conflict-resolution)
|
||||
- **Entity Changes:** [SYNCHRONIZATION.md#entity-changes](SYNCHRONIZATION.md#entity-changes)
|
||||
|
||||
### Scripting
|
||||
- **Frontend Scripts:** [SCRIPTING.md#frontend-scripts](SCRIPTING.md#frontend-scripts)
|
||||
- **Backend Scripts:** [SCRIPTING.md#backend-scripts](SCRIPTING.md#backend-scripts)
|
||||
- **Script Examples:** [SCRIPTING.md#script-examples](SCRIPTING.md#script-examples)
|
||||
- **API Reference:** [SCRIPTING.md#script-api](SCRIPTING.md#script-api)
|
||||
|
||||
### Security
|
||||
- **Authentication:** [SECURITY_ARCHITECTURE.md#authentication](SECURITY_ARCHITECTURE.md#authentication)
|
||||
- **Encryption:** [SECURITY_ARCHITECTURE.md#encryption](SECURITY_ARCHITECTURE.md#encryption)
|
||||
- **Input Sanitization:** [SECURITY_ARCHITECTURE.md#input-sanitization](SECURITY_ARCHITECTURE.md#input-sanitization)
|
||||
- **Best Practices:** [SECURITY_ARCHITECTURE.md#security-best-practices](SECURITY_ARCHITECTURE.md#security-best-practices)
|
||||
|
||||
## 📚 Learning Paths
|
||||
|
||||
### New to Trilium Development
|
||||
1. Read [ARCHITECTURE.md](ARCHITECTURE.md) - System overview
|
||||
2. Setup environment: [Environment Setup](Developer%20Guide/Developer%20Guide/Environment%20Setup.md)
|
||||
3. Explore [DATABASE.md](DATABASE.md) - Understand data model
|
||||
4. Check [Developer Guide](Developer%20Guide/Developer%20Guide/)
|
||||
|
||||
### Want to Create Scripts
|
||||
1. Read [SCRIPTING.md](SCRIPTING.md) - Complete guide
|
||||
2. Check [Script API](Script%20API/) - API reference
|
||||
3. Review examples: [SCRIPTING.md#script-examples](SCRIPTING.md#script-examples)
|
||||
4. Explore [Advanced Showcases](https://triliumnext.github.io/Docs/Wiki/advanced-showcases)
|
||||
|
||||
### Setting Up Sync
|
||||
1. Understand protocol: [SYNCHRONIZATION.md](SYNCHRONIZATION.md)
|
||||
2. Configure server: [SYNCHRONIZATION.md#sync-server-configuration](SYNCHRONIZATION.md#sync-server-configuration)
|
||||
3. Setup clients: [SYNCHRONIZATION.md#client-setup](SYNCHRONIZATION.md#client-setup)
|
||||
4. Troubleshoot: [SYNCHRONIZATION.md#troubleshooting](SYNCHRONIZATION.md#troubleshooting)
|
||||
|
||||
### Security Review
|
||||
1. Read threat model: [SECURITY_ARCHITECTURE.md#threat-model](SECURITY_ARCHITECTURE.md#threat-model)
|
||||
2. Review authentication: [SECURITY_ARCHITECTURE.md#authentication](SECURITY_ARCHITECTURE.md#authentication)
|
||||
3. Check encryption: [SECURITY_ARCHITECTURE.md#encryption](SECURITY_ARCHITECTURE.md#encryption)
|
||||
4. Verify best practices: [SECURITY_ARCHITECTURE.md#security-best-practices](SECURITY_ARCHITECTURE.md#security-best-practices)
|
||||
|
||||
## 🗺️ Documentation Map
|
||||
|
||||
```
|
||||
docs/
|
||||
├── TECHNICAL_DOCUMENTATION.md ← START HERE (Index)
|
||||
│
|
||||
├── Core Technical Docs
|
||||
│ ├── ARCHITECTURE.md (System design)
|
||||
│ ├── DATABASE.md (Data layer)
|
||||
│ ├── SYNCHRONIZATION.md (Sync system)
|
||||
│ ├── SCRIPTING.md (User scripting)
|
||||
│ └── SECURITY_ARCHITECTURE.md (Security)
|
||||
│
|
||||
├── Developer Guide/
|
||||
│ └── Developer Guide/ (Development setup)
|
||||
│
|
||||
├── User Guide/
|
||||
│ └── User Guide/ (End-user docs)
|
||||
│
|
||||
└── Script API/ (API reference)
|
||||
```
|
||||
|
||||
## 💡 Tips for Reading Documentation
|
||||
|
||||
1. **Start with the index:** [TECHNICAL_DOCUMENTATION.md](TECHNICAL_DOCUMENTATION.md) provides an overview
|
||||
2. **Use search:** Press Ctrl+F / Cmd+F to find specific topics
|
||||
3. **Follow links:** Documents are cross-referenced for easy navigation
|
||||
4. **Code examples:** Most docs include practical code examples
|
||||
5. **See Also sections:** Check bottom of each doc for related resources
|
||||
|
||||
## 🔗 External Resources
|
||||
|
||||
- **Website:** https://triliumnotes.org
|
||||
- **Online Docs:** 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
|
||||
|
||||
## 🤝 Contributing to Documentation
|
||||
|
||||
Found an error or want to improve the docs? See:
|
||||
- [Contributing Guide](../README.md#-contribute)
|
||||
- [Documentation Standards](TECHNICAL_DOCUMENTATION.md#documentation-conventions)
|
||||
|
||||
---
|
||||
|
||||
**Version:** 0.99.3
|
||||
**Last Updated:** November 2025
|
||||
**Maintained by:** TriliumNext Team
|
||||
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:
|
||||
|
||||
@@ -9,4 +22,11 @@ Please see the [main documentation](index.md) or visit one of our translated ver
|
||||
- [简体中文](README-ZH_CN.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).
|
||||
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
|
||||
2
docs/User Guide/User Guide/AI.md
vendored
2
docs/User Guide/User Guide/AI.md
vendored
@@ -21,7 +21,7 @@ You will then need to set up the AI “provider” that you wish to use to creat
|
||||
|
||||
In the following example, we're going to use our self-hosted Ollama instance to create the embeddings for our Notes. You can see additional documentation about installing your own Ollama locally in <a class="reference-link" href="AI/Providers/Ollama/Installing%20Ollama.md">Installing Ollama</a>.
|
||||
|
||||
To see what embedding models Ollama has available, you can check out [this search](https://ollama.com/search?c=embedding) on their website, and then `pull` whichever one you want to try out. A popular choice is `mxbai-embed-large`.
|
||||
To see what embedding models Ollama has available, you can check out [this search](https://ollama.com/search?c=embedding)on their website, and then `pull` whichever one you want to try out. As of 4/15/25, my personal favorite is `mxbai-embed-large`.
|
||||
|
||||
First, we'll need to select the Ollama provider from the tabs of providers, then we will enter in the Base URL for our Ollama. Since our Ollama is running on our local machine, our Base URL is `http://localhost:11434`. We will then hit the “refresh” button to have it fetch our models:
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
|
||||
In Trilium, attributes are key-value pairs assigned to notes, providing additional metadata or functionality. There are two primary types of attributes:
|
||||
|
||||
1. <a class="reference-link" href="Attributes/Labels.md">Labels</a> can be used for a variety of purposes, such as storing metadata or configuring the behavior of notes. Labels are also searchable, enhancing note retrieval.
|
||||
1. <a class="reference-link" href="Attributes/Labels.md">Labels</a> can be used for a variety of purposes, such as storing metadata or configuring the behaviour of notes. Labels are also searchable, enhancing note retrieval.
|
||||
|
||||
For more information, including predefined labels, see <a class="reference-link" href="Attributes/Labels.md">Labels</a>.
|
||||
2. <a class="reference-link" href="Attributes/Relations.md">Relations</a> define connections between notes, similar to links. These can be used for metadata and scripting purposes.
|
||||
|
||||
For more information, including a list of predefined relations, see <a class="reference-link" href="Attributes/Relations.md">Relations</a>.
|
||||
|
||||
These attributes play a crucial role in organizing, categorizing, and enhancing the functionality of notes.
|
||||
These attributes play a crucial role in organizing, categorising, and enhancing the functionality of notes.
|
||||
|
||||
## Viewing the list of attributes
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ const {secret, title, content} = req.body;
|
||||
if (req.method == 'POST' && secret === 'secret-password') {
|
||||
// notes must be saved somewhere in the tree hierarchy specified by a parent note.
|
||||
// This is defined by a relation from this code note to the "target" parent note
|
||||
// alternatively you can just use constant noteId for simplicity (get that from "Note Info" dialog of the desired parent note)
|
||||
// alternetively you can just use constant noteId for simplicity (get that from "Note Info" dialog of the desired parent note)
|
||||
const targetParentNoteId = api.currentNote.getRelationValue('targetNote');
|
||||
|
||||
const {note} = api.createTextNote(targetParentNoteId, title, content);
|
||||
@@ -37,7 +37,7 @@ This script note has also following two attributes:
|
||||
Let's test this by using an HTTP client to send a request:
|
||||
|
||||
```
|
||||
POST http://your-trilium-server/custom/create-note
|
||||
POST http://my.trilium.org/custom/create-note
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
@@ -70,7 +70,7 @@ For more information, see [Custom Resource Providers](Custom%20Resource%20Provi
|
||||
REST request paths often contain parameters in the URL, e.g.:
|
||||
|
||||
```
|
||||
http://your-trilium-server/custom/notes/123
|
||||
http://my.trilium.org/custom/notes/123
|
||||
```
|
||||
|
||||
The last part is dynamic so the matching of the URL must also be dynamic - for this reason the matching is done with regular expressions. Following `customRequestHandler` value would match it:
|
||||
@@ -85,4 +85,4 @@ Additionally, this also defines a matching group with the use of parenthesis whi
|
||||
const noteId = api.pathParams[0];
|
||||
```
|
||||
|
||||
Often you also need query params (as in e.g. `http://your-trilium-server/custom/notes?noteId=123`), you can get those with standard express `req.query.noteId`.
|
||||
Often you also need query params (as in e.g. `http://my.trilium.org/custom/notes?noteId=123`), you can get those with standard express `req.query.noteId`.
|
||||
@@ -13,7 +13,7 @@ Note search enables you to find notes by searching for text in the title, conten
|
||||
To search for notes, click on the magnifying glass icon on the toolbar or press the keyboard [shortcut](../Keyboard%20Shortcuts.md).
|
||||
|
||||
1. Set the text to search for in the _Search string_ field.
|
||||
1. Apart from searching for words literally, there is also the possibility to search for attributes or properties of notes.
|
||||
1. Apart from searching for words ad-literam, there is also the possibility to search for attributes or properties of notes.
|
||||
2. See the examples below for more information.
|
||||
2. To limit the search to a note and its sub-children, set a note in _Ancestor_.
|
||||
1. This value is also pre-filled if the search is triggered from a [hoisted note](Note%20Hoisting.md) or a [workspace](Workspaces.md).
|
||||
|
||||
@@ -25,7 +25,7 @@ When you delete a note in Trilium, it is actually only marked for deletion (soft
|
||||
|
||||
Within (by default) 7 days, it is possible to undelete these soft-deleted notes - open the <a class="reference-link" href="UI%20Elements/Recent%20Changes.md">Recent Changes</a> dialog, and you will see a list of all modified notes including the deleted ones. Notes available for undeletion have a link to do so. This is kind of "trash can" functionality known from e.g. Windows.
|
||||
|
||||
Clicking an undelete will recover the note, its content and attributes - note should be just as before being deleted. This action will also undelete note's children which have been deleted in the same action.
|
||||
Clicking an undelete will recover the note, it's content and attributes - note should be just as before being deleted. This action will also undelete note's children which have been deleted in the same action.
|
||||
|
||||
To be able to undelete a note, it is necessary that deleted note's parent must be undeleted (otherwise there's no place where we can undelete it to). This might become a problem when you delete more notes in succession - the solution is then undelete in the reverse order of your deletion.
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ If you are using the _Fixed_ formatting toolbar, all the formatting buttons for
|
||||
* As a more advanced use, it's possible to change the note type in order to modify the [source code](../../Advanced%20Usage/Note%20source.md) of a note.
|
||||
* _**Protect the note**_ toggles whether the current note is encrypted and accessible only by entering the protected session. See [Protected Notes](../Notes/Protected%20Notes.md) for more information.
|
||||
* _**Editable**_ changes whether the current note:
|
||||
* Enters [read-only mode](../Notes/Read-Only%20Notes.md) automatically if the note is too big (default behavior).
|
||||
* Enters [read-only mode](../Notes/Read-Only%20Notes.md) automatically if the note is too big (default behaviour).
|
||||
* Is always in read-only mode (however it can still be edited temporarily).
|
||||
* Is always editable, regardless of its size.
|
||||
* _**Bookmark**_ toggles the display of the current note into the [Launch Bar](Launch%20Bar.md) for easy access. See [Bookmarks](../Navigation/Bookmarks.md) for more information.
|
||||
|
||||
4
docs/User Guide/User Guide/Collections.md
vendored
4
docs/User Guide/User Guide/Collections.md
vendored
@@ -1,5 +1,5 @@
|
||||
# Collections
|
||||
Collections are a unique type of note that don't have content, but instead display their child notes in various presentation methods.
|
||||
Collections are a unique type of notes that don't have a content, but instead display its child notes in various presentation methods.
|
||||
|
||||
## Main collections
|
||||
|
||||
@@ -28,7 +28,7 @@ To change the configuration of a collection or even switch to a different collec
|
||||
|
||||
## Archived notes
|
||||
|
||||
By default, [archived notes](Basic%20Concepts%20and%20Features/Notes/Archived%20Notes.md) will not be shown in collections. This behavior can be changed by going to _Collection Properties_ in the <a class="reference-link" href="Basic%20Concepts%20and%20Features/UI%20Elements/Ribbon.md">Ribbon</a> and checking _Show archived notes_.
|
||||
By default, [archived notes](Basic%20Concepts%20and%20Features/Notes/Archived%20Notes.md) will not be shown in collections. This behaviour can be changed by going to _Collection Properties_ in the <a class="reference-link" href="Basic%20Concepts%20and%20Features/UI%20Elements/Ribbon.md">Ribbon</a> and checking _Show archived notes_.
|
||||
|
||||
Archived notes will be generally indicated by being greyed out as opposed to the normal ones.
|
||||
|
||||
|
||||
@@ -16,4 +16,4 @@ Trilium offers various startup scripts to customize your experience:
|
||||
|
||||
## Synchronization
|
||||
|
||||
For Trilium desktop users who wish to synchronize their data with a server instance, refer to the <a class="reference-link" href="Synchronization.md">Synchronization</a> guide for detailed instructions.
|
||||
For Trilium desktp users who wish to synchronize their data with a server instance, refer to the <a class="reference-link" href="Synchronization.md">Synchronization</a> guide for detailed instructions.
|
||||
@@ -32,7 +32,7 @@ export TRILIUM_DATA_DIR=/home/myuser/data/my-trilium-data
|
||||
|
||||
### Disabling / Modifying the Upload Limit
|
||||
|
||||
If you're running into the 250MB limit imposed on the server by default, and you'd like to increase the upload limit, you can set the `TRILIUM_NO_UPLOAD_LIMIT` environment variable to `true` to disable it completely:
|
||||
If you're running into the 250MB limit imposed on the server by default, and you'd like to increase the upload limit, you can set the `TRILIUM_NO_UPLOAD_LIMIT` environment variable to `true` disable it completely:
|
||||
|
||||
```
|
||||
export TRILIUM_NO_UPLOAD_LIMIT=true
|
||||
|
||||
2
docs/User Guide/User Guide/Note Types.md
vendored
2
docs/User Guide/User Guide/Note Types.md
vendored
@@ -1,5 +1,5 @@
|
||||
# Note Types
|
||||
One of the core features of Trilium is that it supports multiple types of notes, depending on the need.
|
||||
One core features of Trilium is that it supports multiple types of notes, depending on the need.
|
||||
|
||||
## Creating a new note with a different type via the note tree
|
||||
|
||||
|
||||
4
docs/User Guide/User Guide/Scripting.md
vendored
4
docs/User Guide/User Guide/Scripting.md
vendored
@@ -1,14 +1,14 @@
|
||||
# Scripting
|
||||
Trilium supports creating <a class="reference-link" href="Note%20Types/Code.md">Code</a> notes, i.e. notes which allow you to store some programming code and highlight it. Special case is JavaScript code notes which can also be executed inside Trilium which can in conjunction with <a class="reference-link" href="Scripting/Script%20API.md">Script API</a> provide extra functionality.
|
||||
|
||||
## Architecture Overview
|
||||
## Scripting
|
||||
|
||||
To go further I must explain basic architecture of Trilium - in its essence it is a classic web application - it has these two main components:
|
||||
|
||||
* frontend running in the browser (using HTML, CSS, JavaScript) - this is mainly used to interact with the user, display notes etc.
|
||||
* backend running JavaScript code in node.js runtime - this is responsible for e.g. storing notes, encrypting them etc.
|
||||
|
||||
So we have frontend and backend, each with their own set of responsibilities, but their common feature is that they both run JavaScript code. Add to this the fact, that we're able to create JavaScript <a class="reference-link" href="Note%20Types/Code.md">code notes</a> and we're onto something.
|
||||
So we have frontend and backend, each with their own set of responsibilities, but their common feature is that they both run JavaScript code. Add to this the fact, that we're able to create JavaScript \[\[code notes\]\] and we're onto something.
|
||||
|
||||
## Use cases
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Troubleshooting
|
||||
While Trilium is actively maintained and stable, encountering bugs is possible.
|
||||
As Trilium is currently in beta, encountering bugs is to be expected.
|
||||
|
||||
## General Quick Fix
|
||||
|
||||
@@ -21,7 +21,7 @@ TRILIUM_START_NOTE_ID=root ./trilium
|
||||
|
||||
## Broken Script Prevents Application Startup
|
||||
|
||||
If a custom script causes Trilium to crash, and it is set as a startup script or in an active [custom widget](Scripting/Custom%20Widgets.md), start Trilium in "safe mode" to prevent any custom scripts from executing:
|
||||
If a custom script causes Triliumto crash, and it is set as a startup script or in an active [custom widget](Scripting/Custom%20Widgets.md), start Triliumin "safe mode" to prevent any custom scripts from executing:
|
||||
|
||||
```
|
||||
TRILIUM_SAFE_MODE=true ./trilium
|
||||
|
||||
22
docs/index.md
vendored
22
docs/index.md
vendored
@@ -21,27 +21,27 @@ Trilium Notes is a powerful, feature-rich note-taking application designed for b
|
||||
|
||||
<div class="grid cards" markdown>
|
||||
|
||||
- :material-rocket-launch-outline: **[Quick Start Guide](User%20Guide/User%20Guide/Quick%20Start.md)**
|
||||
- :material-rocket-launch-outline: **[Quick Start Guide](User%20Guide/quick-start.md)**
|
||||
|
||||
Get up and running with Trilium in minutes
|
||||
|
||||
- :material-download: **[Desktop Installation](User%20Guide/User%20Guide/Installation%20%26%20Setup/Desktop%20Installation.md)**
|
||||
- :material-download: **[Installation](User%20Guide/installation.md)**
|
||||
|
||||
Download and install Trilium on your desktop
|
||||
Download and install Trilium on your platform
|
||||
|
||||
- :material-server: **[Server Installation](User%20Guide/User%20Guide/Installation%20%26%20Setup/Server%20Installation.md)**
|
||||
- :material-docker: **[Docker Setup](User%20Guide/docker.md)**
|
||||
|
||||
Deploy Trilium as a server
|
||||
Deploy Trilium using Docker containers
|
||||
|
||||
- :material-book-open-variant: **[User Guide](User%20Guide/User%20Guide.md)**
|
||||
- :material-book-open-variant: **[User Guide](User%20Guide/index.md)**
|
||||
|
||||
Comprehensive guide to all features
|
||||
|
||||
- :material-code-braces: **[Script API](Script%20API/index.html)**
|
||||
- :material-code-braces: **[Script API](Script%20API/index.md)**
|
||||
|
||||
Automate and extend Trilium with scripting
|
||||
|
||||
- :material-wrench: **[Developer Guide](Developer%20Guide/Developer%20Guide/Environment%20Setup.md)**
|
||||
- :material-wrench: **[Developer Guide](Developer%20Guide/index.md)**
|
||||
|
||||
Contributing and development documentation
|
||||
|
||||
@@ -80,14 +80,14 @@ Trilium Notes is a powerful, feature-rich note-taking application designed for b
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **[FAQ](User%20Guide/User%20Guide/FAQ.md)** - Frequently asked questions
|
||||
- **[Troubleshooting](User%20Guide/User%20Guide/Troubleshooting.md)** - Common issues and solutions
|
||||
- **[FAQ](support/faq.md)** - Frequently asked questions
|
||||
- **[Troubleshooting](support/troubleshooting.md)** - Common issues and solutions
|
||||
- **[Community Forum](https://github.com/triliumnext/trilium/discussions)** - Ask questions and share tips
|
||||
- **[Issue Tracker](https://github.com/triliumnext/trilium/issues)** - Report bugs and request features
|
||||
|
||||
## Contributing
|
||||
|
||||
Trilium is open-source and welcomes contributions! Check out our [GitHub repository](https://github.com/triliumnext/trilium) to get started.
|
||||
Trilium is open-source and welcomes contributions! Check out our [Contributing Guide](Developer%20Guide/contributing.md) to get started.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
Reference in New Issue
Block a user