Update docs

This commit is contained in:
Andras Schmelczer 2025-11-22 12:13:22 +00:00
parent 38810579ec
commit 00d2061627
20 changed files with 1149 additions and 569 deletions

View file

@ -42,6 +42,11 @@ jobs:
cd docs cd docs
npm ci npm ci
- name: Check formatting
run: |
cd docs
npm run format:check
- name: Build documentation - name: Build documentation
run: | run: |
cd docs cd docs

4
docs/.prettierignore Normal file
View file

@ -0,0 +1,4 @@
node_modules/
.vitepress/dist/
.vitepress/cache/
package-lock.json

19
docs/.prettierrc Normal file
View file

@ -0,0 +1,19 @@
{
"printWidth": 120,
"tabWidth": 4,
"useTabs": true,
"semi": false,
"singleQuote": false,
"trailingComma": "none",
"endOfLine": "lf",
"proseWrap": "preserve",
"overrides": [
{
"files": "*.md",
"options": {
"proseWrap": "preserve",
"printWidth": 120
}
}
]
}

View file

@ -1,62 +1,59 @@
import { defineConfig } from 'vitepress' import { defineConfig } from "vitepress"
export default defineConfig({ export default defineConfig({
title: 'VaultLink', title: "VaultLink",
description: 'Self-hosted real-time synchronization for Obsidian', description: "Self-hosted real-time synchronization for Obsidian",
base: '/vault-link/', base: "/vault-link/",
themeConfig: { themeConfig: {
logo: '/logo.svg', logo: "/logo.svg",
nav: [ nav: [
{ text: 'Home', link: '/' }, { text: "Home", link: "/" },
{ text: 'Guide', link: '/guide/getting-started' }, { text: "Guide", link: "/guide/getting-started" },
{ text: 'Architecture', link: '/architecture/' }, { text: "Architecture", link: "/architecture/" },
{ text: 'GitHub', link: 'https://github.com/schmelczer/vault-link' } { text: "GitHub", link: "https://github.com/schmelczer/vault-link" }
], ],
sidebar: [ sidebar: [
{ {
text: 'Introduction', text: "Introduction",
items: [ items: [
{ text: 'What is VaultLink?', link: '/guide/what-is-vaultlink' }, { text: "What is VaultLink?", link: "/guide/what-is-vaultlink" },
{ text: 'Getting Started', link: '/guide/getting-started' } { text: "Getting Started", link: "/guide/getting-started" },
{ text: "Comparison with Alternatives", link: "/guide/alternatives" }
] ]
}, },
{ {
text: 'Setup', text: "Setup",
items: [ items: [
{ text: 'Server Setup', link: '/guide/server-setup' }, { text: "Server Setup", link: "/guide/server-setup" },
{ text: 'Obsidian Plugin', link: '/guide/obsidian-plugin' }, { text: "Obsidian Plugin", link: "/guide/obsidian-plugin" },
{ text: 'CLI Client', link: '/guide/cli-client' } { text: "CLI Client", link: "/guide/cli-client" }
] ]
}, },
{ {
text: 'Configuration', text: "Configuration",
items: [ items: [
{ text: 'Server Configuration', link: '/config/server' }, { text: "Server Configuration", link: "/config/server" },
{ text: 'Authentication', link: '/config/authentication' }, { text: "Authentication", link: "/config/authentication" },
{ text: 'Advanced Options', link: '/config/advanced' } { text: "Advanced Options", link: "/config/advanced" }
] ]
}, },
{ {
text: 'Architecture', text: "Architecture",
items: [ items: [
{ text: 'Overview', link: '/architecture/' }, { text: "Overview", link: "/architecture/" },
{ text: 'Sync Algorithm', link: '/architecture/sync-algorithm' }, { text: "Sync Algorithm", link: "/architecture/sync-algorithm" },
{ text: 'Data Flow', link: '/architecture/data-flow' } { text: "Data Flow", link: "/architecture/data-flow" }
] ]
} }
], ],
socialLinks: [ socialLinks: [{ icon: "github", link: "https://github.com/schmelczer/vault-link" }],
{ icon: 'github', link: 'https://github.com/schmelczer/vault-link' }
],
footer: { footer: {
message: 'Released under the MIT License.', message: "Released under the MIT License.",
copyright: 'Copyright © 2024-present Andras Schmelczer' copyright: "Copyright © 2024-present Andras Schmelczer"
}, },
search: { search: {
provider: 'local' provider: "local"
} }
}, },
head: [ head: [["link", { rel: "icon", type: "image/svg+xml", href: "/vault-link/logo.svg" }]]
['link', { rel: 'icon', type: 'image/svg+xml', href: '/vault-link/logo.svg' }]
]
}) })

View file

@ -44,6 +44,20 @@ Preview the built site:
npm run preview npm run preview
``` ```
### Format
Format all markdown and TypeScript files:
```bash
npm run format
```
Check formatting without making changes:
```bash
npm run format:check
```
## Deployment ## Deployment
The documentation is automatically deployed to GitHub Pages when changes are pushed to the `main` branch. The documentation is automatically deployed to GitHub Pages when changes are pushed to the `main` branch.
@ -81,6 +95,7 @@ docs/
### Markdown Features ### Markdown Features
VitePress supports: VitePress supports:
- GitHub Flavored Markdown - GitHub Flavored Markdown
- Custom containers (tip, warning, danger) - Custom containers (tip, warning, danger)
- Code syntax highlighting - Code syntax highlighting

View file

@ -22,6 +22,7 @@ sequenceDiagram
``` ```
**Steps**: **Steps**:
1. Client initiates WebSocket connection to server 1. Client initiates WebSocket connection to server
2. Server accepts connection 2. Server accepts connection
3. Client sends authentication message with token and vault name 3. Client sends authentication message with token and vault name
@ -72,6 +73,7 @@ sequenceDiagram
``` ```
**Process**: **Process**:
1. Client scans local filesystem 1. Client scans local filesystem
2. Client requests file list from server 2. Client requests file list from server
3. Server queries database and returns metadata 3. Server queries database and returns metadata
@ -106,6 +108,7 @@ sequenceDiagram
``` ```
**Flow**: **Flow**:
1. Filesystem watcher detects local change 1. Filesystem watcher detects local change
2. Client reads file content 2. Client reads file content
3. Client uploads file via WebSocket 3. Client uploads file via WebSocket
@ -325,6 +328,7 @@ CREATE TABLE cursors (
### Queries ### Queries
**Get files since version**: **Get files since version**:
```sql ```sql
SELECT * FROM documents SELECT * FROM documents
WHERE version > ? AND deleted = FALSE WHERE version > ? AND deleted = FALSE
@ -332,6 +336,7 @@ ORDER BY version ASC;
``` ```
**Store new version**: **Store new version**:
```sql ```sql
INSERT INTO versions (document_id, version, content, created_at) INSERT INTO versions (document_id, version, content, created_at)
VALUES (?, ?, ?, ?); VALUES (?, ?, ?, ?);
@ -342,6 +347,7 @@ WHERE id = ?;
``` ```
**Update cursor**: **Update cursor**:
```sql ```sql
INSERT OR REPLACE INTO cursors (client_id, last_version, last_updated) INSERT OR REPLACE INTO cursors (client_id, last_version, last_updated)
VALUES (?, ?, ?); VALUES (?, ?, ?);
@ -352,6 +358,7 @@ VALUES (?, ?, ?);
### Client → Server Messages ### Client → Server Messages
**Upload File**: **Upload File**:
```json ```json
{ {
"type": "upload_file", "type": "upload_file",
@ -363,6 +370,7 @@ VALUES (?, ?, ?);
``` ```
**Download File**: **Download File**:
```json ```json
{ {
"type": "download_file", "type": "download_file",
@ -371,6 +379,7 @@ VALUES (?, ?, ?);
``` ```
**Delete File**: **Delete File**:
```json ```json
{ {
"type": "delete_file", "type": "delete_file",
@ -379,6 +388,7 @@ VALUES (?, ?, ?);
``` ```
**List Files**: **List Files**:
```json ```json
{ {
"type": "list_files", "type": "list_files",
@ -389,6 +399,7 @@ VALUES (?, ?, ?);
### Server → Client Messages ### Server → Client Messages
**File Updated**: **File Updated**:
```json ```json
{ {
"type": "file_updated", "type": "file_updated",
@ -400,6 +411,7 @@ VALUES (?, ?, ?);
``` ```
**File Content**: **File Content**:
```json ```json
{ {
"type": "file_content", "type": "file_content",
@ -410,6 +422,7 @@ VALUES (?, ?, ?);
``` ```
**File Deleted**: **File Deleted**:
```json ```json
{ {
"type": "file_deleted", "type": "file_deleted",
@ -419,6 +432,7 @@ VALUES (?, ?, ?);
``` ```
**Sync Complete**: **Sync Complete**:
```json ```json
{ {
"type": "sync_complete", "type": "sync_complete",
@ -428,6 +442,7 @@ VALUES (?, ?, ?);
``` ```
**Error**: **Error**:
```json ```json
{ {
"type": "error", "type": "error",
@ -441,18 +456,21 @@ VALUES (?, ?, ?);
### Client-Side Errors ### Client-Side Errors
**Network failure**: **Network failure**:
1. Detect WebSocket disconnect 1. Detect WebSocket disconnect
2. Queue pending operations 2. Queue pending operations
3. Retry connection with exponential backoff 3. Retry connection with exponential backoff
4. Replay queued operations on reconnect 4. Replay queued operations on reconnect
**File read error**: **File read error**:
1. Log error 1. Log error
2. Skip file 2. Skip file
3. Continue with other files 3. Continue with other files
4. Report to user 4. Report to user
**Write conflict**: **Write conflict**:
1. Receive updated version from server 1. Receive updated version from server
2. Apply OT merge locally 2. Apply OT merge locally
3. Overwrite local file 3. Overwrite local file
@ -461,16 +479,19 @@ VALUES (?, ?, ?);
### Server-Side Errors ### Server-Side Errors
**Database error**: **Database error**:
1. Log error 1. Log error
2. Return error to client 2. Return error to client
3. Client retries operation 3. Client retries operation
**Invalid operation**: **Invalid operation**:
1. Validate message format 1. Validate message format
2. Return specific error code 2. Return specific error code
3. Client handles error appropriately 3. Client handles error appropriately
**Authentication failure**: **Authentication failure**:
1. Reject connection 1. Reject connection
2. Send auth error 2. Send auth error
3. Client prompts for new credentials 3. Client prompts for new credentials

View file

@ -43,6 +43,7 @@ VaultLink is built as a distributed system with a central sync server and multip
The central authority for synchronization, written in Rust using Axum framework. The central authority for synchronization, written in Rust using Axum framework.
**Responsibilities**: **Responsibilities**:
- Accept WebSocket connections from clients - Accept WebSocket connections from clients
- Authenticate users via token-based auth - Authenticate users via token-based auth
- Store document versions in SQLite - Store document versions in SQLite
@ -51,6 +52,7 @@ The central authority for synchronization, written in Rust using Axum framework.
- Manage vault access control - Manage vault access control
**Technology**: **Technology**:
- **Language**: Rust 1.89+ - **Language**: Rust 1.89+
- **Framework**: Axum (async web framework) - **Framework**: Axum (async web framework)
- **Database**: SQLite with SQLx - **Database**: SQLite with SQLx
@ -62,6 +64,7 @@ The central authority for synchronization, written in Rust using Axum framework.
TypeScript library providing core synchronization logic, used by both the Obsidian plugin and CLI client. TypeScript library providing core synchronization logic, used by both the Obsidian plugin and CLI client.
**Responsibilities**: **Responsibilities**:
- Manage WebSocket connection to server - Manage WebSocket connection to server
- Watch local filesystem for changes - Watch local filesystem for changes
- Upload and download files - Upload and download files
@ -70,6 +73,7 @@ TypeScript library providing core synchronization logic, used by both the Obsidi
- Maintain sync metadata - Maintain sync metadata
**Technology**: **Technology**:
- **Language**: TypeScript - **Language**: TypeScript
- **Build**: Webpack - **Build**: Webpack
- **Protocol**: WebSocket client - **Protocol**: WebSocket client
@ -80,12 +84,14 @@ TypeScript library providing core synchronization logic, used by both the Obsidi
Integration layer between sync client and Obsidian. Integration layer between sync client and Obsidian.
**Responsibilities**: **Responsibilities**:
- Provide UI for configuration - Provide UI for configuration
- Bridge sync client with Obsidian's file system API - Bridge sync client with Obsidian's file system API
- Handle Obsidian lifecycle events - Handle Obsidian lifecycle events
- Display sync status to users - Display sync status to users
**Technology**: **Technology**:
- **Platform**: Obsidian Plugin API - **Platform**: Obsidian Plugin API
- **Core**: sync-client library - **Core**: sync-client library
- **UI**: Obsidian settings UI - **UI**: Obsidian settings UI
@ -95,12 +101,14 @@ Integration layer between sync client and Obsidian.
Standalone executable for syncing vaults without Obsidian. Standalone executable for syncing vaults without Obsidian.
**Responsibilities**: **Responsibilities**:
- Command-line interface - Command-line interface
- File system access via Node.js - File system access via Node.js
- Daemon mode for continuous sync - Daemon mode for continuous sync
- Health check endpoint for monitoring - Health check endpoint for monitoring
**Technology**: **Technology**:
- **Language**: TypeScript - **Language**: TypeScript
- **Runtime**: Node.js - **Runtime**: Node.js
- **CLI**: Commander.js - **CLI**: Commander.js
@ -190,6 +198,7 @@ databases/
``` ```
**Database Schema** (simplified): **Database Schema** (simplified):
- **documents**: File metadata (path, size, modified time) - **documents**: File metadata (path, size, modified time)
- **versions**: Document content with version history - **versions**: Document content with version history
- **cursors**: Client sync state - **cursors**: Client sync state
@ -213,6 +222,7 @@ The `.vaultlink` directory tracks which files have been synced and their version
Client-server communication uses JSON messages over WebSocket. Client-server communication uses JSON messages over WebSocket.
**Message Types**: **Message Types**:
- `upload_file`: Client → Server (file upload) - `upload_file`: Client → Server (file upload)
- `download_file`: Client → Server (request file) - `download_file`: Client → Server (request file)
- `file_updated`: Server → Client (file changed notification) - `file_updated`: Server → Client (file changed notification)
@ -253,11 +263,13 @@ Token-based authentication on connection:
### Scaling Approaches ### Scaling Approaches
**Vertical Scaling**: **Vertical Scaling**:
- Increase server resources (CPU, RAM, storage) - Increase server resources (CPU, RAM, storage)
- Optimize database queries and indexing - Optimize database queries and indexing
- Tune connection limits - Tune connection limits
**Horizontal Scaling** (future): **Horizontal Scaling** (future):
- Separate vault servers (vault sharding) - Separate vault servers (vault sharding)
- Load balancer with sticky sessions - Load balancer with sticky sessions
- Shared storage layer for SQLite databases - Shared storage layer for SQLite databases

View file

@ -9,11 +9,13 @@ Operational transformation is a technique for managing concurrent edits to the s
### Why OT? ### Why OT?
Traditional conflict resolution approaches: Traditional conflict resolution approaches:
- **Last write wins**: Loses data, frustrating for users - **Last write wins**: Loses data, frustrating for users
- **Manual merging**: Interrupts workflow, requires user intervention - **Manual merging**: Interrupts workflow, requires user intervention
- **Version branching**: Complex, not suitable for real-time sync - **Version branching**: Complex, not suitable for real-time sync
Operational transformation: Operational transformation:
- **Automatic**: No user intervention required - **Automatic**: No user intervention required
- **Preserves all edits**: No data loss - **Preserves all edits**: No data loss
- **Real-time**: Changes appear immediately - **Real-time**: Changes appear immediately
@ -23,6 +25,39 @@ Operational transformation:
VaultLink uses the [`reconcile-text`](https://crates.io/crates/reconcile-text) Rust library for operational transformation on text documents. VaultLink uses the [`reconcile-text`](https://crates.io/crates/reconcile-text) Rust library for operational transformation on text documents.
### Why reconcile-text over CRDTs?
VaultLink faces a **differential synchronization** challenge: users edit Obsidian vaults with various editors (Obsidian desktop, Obsidian mobile, Vim, VS Code, or any text editor), often while offline. This means we only observe the **final state** of each document after editing, not the individual keystrokes or operations that produced it.
**The fundamental problem**:
- **CRDTs and traditional OT** require capturing every individual operation (each character insertion, deletion, cursor movement)
- **VaultLink's reality**: Users edit files with arbitrary tools, sync happens after the fact
- **What we know**: Parent version and two modified versions
- **What we don't know**: The sequence of operations that created those modifications
**Why reconcile-text wins for this use case**:
1. **Works with end states only**: reconcile-text performs conflict-free 3-way merging given just parent, left, and right versions—no operation history needed
2. **Editor-agnostic**: Users can edit with any tool without requiring VaultLink-specific plugins or operation tracking
3. **Offline-first**: Edits made while disconnected are merged cleanly when sync resumes, because we're diffing final states rather than replaying operations
4. **No conflict markers**: Unlike Git merge, produces clean merged output without `<<<<<<<` markers that interrupt note-taking flow
5. **Human text forgiveness**: For knowledge bases and documentation, a slightly imperfect merge (e.g., minor word order issues) is vastly preferable to manual conflict resolution
6. **Simpler infrastructure**: No need for complex operation capture, transformation logs, or tombstone management that CRDTs require
**The tradeoff**:
CRDTs excel when you control the entire editing infrastructure and can capture every operation. reconcile-text excels when you're synchronizing independently-edited files—exactly VaultLink's scenario. The merge quality depends on Myers' diff algorithm rather than operation history, which is the correct tradeoff for differential sync.
For note-taking workflows where users value editor freedom and offline editing, this approach provides superior user experience compared to either CRDTs (which would require operation tracking) or Git-style merging (which requires manual conflict resolution).
[Learn more about reconcile-text →](https://schmelczer.dev/reconcile)
### How It Works ### How It Works
Given a base document and two sets of changes, OT produces a merged result that includes both changes. Given a base document and two sets of changes, OT produces a merged result that includes both changes.
@ -41,6 +76,7 @@ OT result: "Hello beautiful world!" (both changes applied)
### Operation Types ### Operation Types
The algorithm handles these operations: The algorithm handles these operations:
- **Insert**: Add text at position - **Insert**: Add text at position
- **Delete**: Remove text from position - **Delete**: Remove text from position
- **Retain**: Keep existing text unchanged - **Retain**: Keep existing text unchanged
@ -62,10 +98,12 @@ VaultLink maintains sync state to track which changes have been applied.
### Version Vectors ### Version Vectors
Each document has a version tracked by: Each document has a version tracked by:
- **Server version**: Incremented on each change - **Server version**: Incremented on each change
- **Client cursors**: Track which version each client has seen - **Client cursors**: Track which version each client has seen
This enables: This enables:
- Efficient syncing (only send changes since last sync) - Efficient syncing (only send changes since last sync)
- Conflict detection (concurrent edits to same version) - Conflict detection (concurrent edits to same version)
- Ordering of operations - Ordering of operations
@ -84,6 +122,7 @@ struct Cursor {
``` ```
On sync: On sync:
1. Client sends cursor (last seen version) 1. Client sends cursor (last seen version)
2. Server returns all changes since that version 2. Server returns all changes since that version
3. Client applies changes and updates cursor 3. Client applies changes and updates cursor
@ -95,21 +134,26 @@ On sync:
Two users edit the same paragraph simultaneously. Two users edit the same paragraph simultaneously.
**Initial state**: **Initial state**:
``` ```
Version 10: "The quick brown fox jumps over the lazy dog." Version 10: "The quick brown fox jumps over the lazy dog."
``` ```
**User A's edit** (version 11): **User A's edit** (version 11):
``` ```
"The quick brown fox jumps over the very lazy dog." "The quick brown fox jumps over the very lazy dog."
``` ```
*Inserts "very " at position 40*
_Inserts "very " at position 40_
**User B's edit** (also from version 10): **User B's edit** (also from version 10):
``` ```
"The quick red fox jumps over the lazy dog." "The quick red fox jumps over the lazy dog."
``` ```
*Replaces "brown" with "red" at position 10*
_Replaces "brown" with "red" at position 10_
### Server Processing ### Server Processing
@ -147,11 +191,13 @@ Both edits are preserved in the final document.
**Scenario**: User A deletes a paragraph while User B edits it. **Scenario**: User A deletes a paragraph while User B edits it.
**Resolution**: **Resolution**:
- OT algorithm prioritizes preservation of content - OT algorithm prioritizes preservation of content
- Insert operation is transformed to account for deletion - Insert operation is transformed to account for deletion
- Typically results in inserted content appearing nearby - Typically results in inserted content appearing nearby
**Example**: **Example**:
``` ```
Base: "Line 1\nLine 2\nLine 3" Base: "Line 1\nLine 2\nLine 3"
@ -160,6 +206,7 @@ User B: Edit Line 2 → "Line 1\nLine 2 modified\nLine 3"
Result: "Line 1\nLine 2 modified\nLine 3" Result: "Line 1\nLine 2 modified\nLine 3"
``` ```
(Insert takes precedence, preserving user content) (Insert takes precedence, preserving user content)
### 2. Overlapping Edits ### 2. Overlapping Edits
@ -167,6 +214,7 @@ Result: "Line 1\nLine 2 modified\nLine 3"
**Scenario**: Two users edit overlapping regions. **Scenario**: Two users edit overlapping regions.
**Resolution**: **Resolution**:
- OT splits operations into non-overlapping segments - OT splits operations into non-overlapping segments
- Applies each segment independently - Applies each segment independently
- Merges results - Merges results
@ -176,6 +224,7 @@ Result: "Line 1\nLine 2 modified\nLine 3"
**Scenario**: Two users delete overlapping text. **Scenario**: Two users delete overlapping text.
**Resolution**: **Resolution**:
- Deletes are merged - Deletes are merged
- Final result has the union of deleted ranges removed - Final result has the union of deleted ranges removed
@ -184,6 +233,7 @@ Result: "Line 1\nLine 2 modified\nLine 3"
**Scenario**: Client loses connection, makes edits offline, reconnects. **Scenario**: Client loses connection, makes edits offline, reconnects.
**Resolution**: **Resolution**:
1. Client queues edits locally 1. Client queues edits locally
2. On reconnect, sends all queued operations 2. On reconnect, sends all queued operations
3. Server applies OT against all operations that happened during partition 3. Server applies OT against all operations that happened during partition
@ -206,6 +256,7 @@ Result: "Line 1\nLine 2 modified\nLine 3"
### Optimization ### Optimization
VaultLink optimizes for: VaultLink optimizes for:
- Small, frequent edits (typical typing patterns) - Small, frequent edits (typical typing patterns)
- Text documents (not binary files) - Text documents (not binary files)
- Real-time processing (no batching delay) - Real-time processing (no batching delay)
@ -215,6 +266,7 @@ VaultLink optimizes for:
### Binary Files ### Binary Files
OT works best for text files. Binary files: OT works best for text files. Binary files:
- Cannot be meaningfully merged - Cannot be meaningfully merged
- Use last-write-wins strategy - Use last-write-wins strategy
- May cause data loss on concurrent edits - May cause data loss on concurrent edits
@ -224,6 +276,7 @@ OT works best for text files. Binary files:
### Large Documents ### Large Documents
Very large documents (> 1MB) may have: Very large documents (> 1MB) may have:
- Higher transformation costs - Higher transformation costs
- Slower sync times - Slower sync times
- Increased memory usage - Increased memory usage
@ -233,6 +286,7 @@ Very large documents (> 1MB) may have:
### Complex Formatting ### Complex Formatting
Markdown with complex structures may occasionally produce unexpected results: Markdown with complex structures may occasionally produce unexpected results:
- Nested lists - Nested lists
- Tables - Tables
- Code blocks - Code blocks
@ -244,6 +298,7 @@ Markdown with complex structures may occasionally produce unexpected results:
### Strong Consistency ### Strong Consistency
VaultLink provides **strong eventual consistency**: VaultLink provides **strong eventual consistency**:
- All clients eventually converge to the same state - All clients eventually converge to the same state
- Operations applied in causal order - Operations applied in causal order
- No data loss under normal operation - No data loss under normal operation
@ -265,7 +320,7 @@ VaultLink provides **strong eventual consistency**:
### Git-style Merging ### Git-style Merging
| Aspect | Git Merge | VaultLink OT | | Aspect | Git Merge | VaultLink OT |
|--------|-----------|--------------| | -------------------------- | ------------ | ----------------------- |
| Real-time | No | Yes | | Real-time | No | Yes |
| Manual conflict resolution | Yes | No | | Manual conflict resolution | Yes | No |
| Branching | Yes | No | | Branching | Yes | No |
@ -274,18 +329,22 @@ VaultLink provides **strong eventual consistency**:
### CRDTs (Conflict-free Replicated Data Types) ### CRDTs (Conflict-free Replicated Data Types)
| Aspect | CRDTs | VaultLink OT | | Aspect | CRDTs | VaultLink (reconcile-text) |
|--------|-------|--------------| | ----------------------------- | ------------------------------------ | ------------------------------------------------- |
| Server required | No | Yes | | **Operation tracking** | Required (every keystroke) | Not required (end states only) |
| Memory overhead | Higher | Lower | | **Editor freedom** | Limited (must use CRDT-aware editor) | Unlimited (any text editor works) |
| Complexity | Higher | Lower | | **Offline editing** | Requires operation log | Works with file comparison |
| Deletion handling | Complex (tombstones) | Simple | | **Server required** | No | Yes |
| Best for | Distributed systems | Centralized sync | | **Memory overhead** | Higher (tombstones, metadata) | Lower (versions only) |
| **Infrastructure complexity** | Higher | Lower |
| **Best for** | Controlled editing environments | Independent file editing (Obsidian, Vim, VS Code) |
**Key insight**: CRDTs are superior when you can capture every operation. reconcile-text is superior when users edit files independently with arbitrary tools—exactly VaultLink's scenario.
### Last Write Wins ### Last Write Wins
| Aspect | LWW | VaultLink OT | | Aspect | LWW | VaultLink OT |
|--------|-----|--------------| | --------------- | ---- | ------------ |
| Data loss | Yes | No | | Data loss | Yes | No |
| Simplicity | High | Medium | | Simplicity | High | Medium |
| User experience | Poor | Excellent | | User experience | Poor | Excellent |

View file

@ -13,11 +13,13 @@ While VaultLink handles most SQLite configuration automatically, you can optimiz
VaultLink uses Write-Ahead Logging (WAL) mode by default for better concurrency. VaultLink uses Write-Ahead Logging (WAL) mode by default for better concurrency.
**Benefits**: **Benefits**:
- Readers don't block writers - Readers don't block writers
- Writers don't block readers - Writers don't block readers
- Better performance for concurrent access - Better performance for concurrent access
**Maintenance**: **Maintenance**:
```bash ```bash
# Checkpoint WAL to main database (run periodically) # Checkpoint WAL to main database (run periodically)
sqlite3 databases/vault.db "PRAGMA wal_checkpoint(TRUNCATE);" sqlite3 databases/vault.db "PRAGMA wal_checkpoint(TRUNCATE);"
@ -39,6 +41,7 @@ sqlite3 databases/vault.db "ANALYZE;"
``` ```
**Schedule maintenance**: **Schedule maintenance**:
```bash ```bash
#!/bin/bash #!/bin/bash
# monthly-maintenance.sh # monthly-maintenance.sh
@ -83,6 +86,7 @@ max_connections = (concurrent_users × avg_operations_per_user) + buffer
``` ```
**Example**: **Example**:
- 20 concurrent users - 20 concurrent users
- 2 operations per user on average - 2 operations per user on average
- 25% buffer - 25% buffer
@ -96,6 +100,7 @@ max_connections = (20 × 2) × 1.25 = 50
Adjust timeouts based on network characteristics: Adjust timeouts based on network characteristics:
**Fast local network**: **Fast local network**:
```yaml ```yaml
database: database:
cursor_timeout_seconds: 30 cursor_timeout_seconds: 30
@ -105,6 +110,7 @@ server:
``` ```
**Slow or unreliable network**: **Slow or unreliable network**:
```yaml ```yaml
database: database:
cursor_timeout_seconds: 180 cursor_timeout_seconds: 180
@ -114,6 +120,7 @@ server:
``` ```
**Mobile clients**: **Mobile clients**:
```yaml ```yaml
database: database:
cursor_timeout_seconds: 300 # Longer for intermittent connections cursor_timeout_seconds: 300 # Longer for intermittent connections
@ -257,10 +264,10 @@ services:
deploy: deploy:
resources: resources:
limits: limits:
cpus: '2.0' cpus: "2.0"
memory: 4G memory: 4G
reservations: reservations:
cpus: '1.0' cpus: "1.0"
memory: 2G memory: 2G
``` ```
@ -375,6 +382,7 @@ find "$BACKUP_DIR" -name "vaultlink-*.tar.gz" -mtime +$RETENTION_DAYS -delete
``` ```
Schedule with cron: Schedule with cron:
```cron ```cron
0 2 * * * /opt/vaultlink/backup-vaultlink.sh 0 2 * * * /opt/vaultlink/backup-vaultlink.sh
``` ```
@ -530,16 +538,19 @@ services:
### Moving to New Server ### Moving to New Server
1. **Backup on old server**: 1. **Backup on old server**:
```bash ```bash
./backup-vaultlink.sh ./backup-vaultlink.sh
``` ```
2. **Transfer backup**: 2. **Transfer backup**:
```bash ```bash
scp vaultlink-backup.tar.gz new-server:/tmp/ scp vaultlink-backup.tar.gz new-server:/tmp/
``` ```
3. **Restore on new server**: 3. **Restore on new server**:
```bash ```bash
./restore-vaultlink.sh /tmp/vaultlink-backup.tar.gz ./restore-vaultlink.sh /tmp/vaultlink-backup.tar.gz
``` ```

View file

@ -5,6 +5,7 @@ VaultLink uses token-based authentication with per-user vault access control. Th
## Overview ## Overview
Authentication in VaultLink: Authentication in VaultLink:
- **Token-based**: Users authenticate with secure tokens - **Token-based**: Users authenticate with secure tokens
- **Configured in YAML**: All users defined in `config.yml` - **Configured in YAML**: All users defined in `config.yml`
- **Vault-level access**: Control which vaults each user can access - **Vault-level access**: Control which vaults each user can access
@ -35,6 +36,7 @@ Human-readable identifier for the user. Used in logs and auditing.
``` ```
**Notes**: **Notes**:
- Must be unique across all users - Must be unique across all users
- Used for identification only, not authentication - Used for identification only, not authentication
- Appears in server logs - Appears in server logs
@ -52,6 +54,7 @@ Authentication token for the user. Must be kept secret.
``` ```
**Best practices**: **Best practices**:
- Generate with: `openssl rand -hex 32` - Generate with: `openssl rand -hex 32`
- Minimum length: 32 characters - Minimum length: 32 characters
- Use different token per user - Use different token per user
@ -59,6 +62,7 @@ Authentication token for the user. Must be kept secret.
- Rotate periodically - Rotate periodically
**Example token generation**: **Example token generation**:
```bash ```bash
# Generate a secure token # Generate a secure token
openssl rand -hex 32 openssl rand -hex 32
@ -73,6 +77,7 @@ openssl rand -hex 32
Defines which vaults the user can access. Defines which vaults the user can access.
**Three modes**: **Three modes**:
1. `allow_access_to_all`: Access to all vaults 1. `allow_access_to_all`: Access to all vaults
2. `allow_list`: Access to specific vaults only 2. `allow_list`: Access to specific vaults only
3. `deny_list`: Access to all vaults except specific ones 3. `deny_list`: Access to all vaults except specific ones
@ -93,6 +98,7 @@ users:
``` ```
**Use cases**: **Use cases**:
- Administrator accounts - Administrator accounts
- Personal single-user deployments - Personal single-user deployments
- Development/testing - Development/testing
@ -115,11 +121,13 @@ users:
``` ```
**Use cases**: **Use cases**:
- Multi-user deployments - Multi-user deployments
- Restricted access scenarios - Restricted access scenarios
- Separation of concerns - Separation of concerns
**Notes**: **Notes**:
- User can only access listed vaults - User can only access listed vaults
- Attempting to access other vaults returns authentication error - Attempting to access other vaults returns authentication error
- Empty list = no access to any vault - Empty list = no access to any vault
@ -141,10 +149,12 @@ users:
``` ```
**Use cases**: **Use cases**:
- Users with broad access except sensitive vaults - Users with broad access except sensitive vaults
- Simplify configuration when most vaults are accessible - Simplify configuration when most vaults are accessible
**Notes**: **Notes**:
- User can access any vault not in the deny list - User can access any vault not in the deny list
- Attempting to access denied vaults returns authentication error - Attempting to access denied vaults returns authentication error
@ -248,6 +258,7 @@ users:
### Validation ### Validation
Server checks: Server checks:
1. **Token match**: Token exists in `user_configs` 1. **Token match**: Token exists in `user_configs`
2. **Vault access**: User has permission for vault 2. **Vault access**: User has permission for vault
3. **Connection limits**: Not exceeding `max_clients_per_vault` 3. **Connection limits**: Not exceeding `max_clients_per_vault`
@ -255,16 +266,19 @@ Server checks:
### Errors ### Errors
**Invalid token**: **Invalid token**:
``` ```
Authentication failed: Invalid token Authentication failed: Invalid token
``` ```
**No vault access**: **No vault access**:
``` ```
Authentication failed: User does not have access to vault 'restricted' Authentication failed: User does not have access to vault 'restricted'
``` ```
**Connection limit**: **Connection limit**:
``` ```
Connection rejected: Maximum clients reached for vault Connection rejected: Maximum clients reached for vault
``` ```
@ -289,6 +303,7 @@ uuidgen
### Token Storage ### Token Storage
**In config file**: **In config file**:
```yaml ```yaml
users: users:
user_configs: user_configs:
@ -297,6 +312,7 @@ users:
``` ```
**Load from environment**: **Load from environment**:
```bash ```bash
export ALICE_TOKEN="$(openssl rand -hex 32)" export ALICE_TOKEN="$(openssl rand -hex 32)"
./sync_server config.yml ./sync_server config.yml
@ -314,11 +330,13 @@ Periodically change tokens:
### Token Revocation ### Token Revocation
To revoke access: To revoke access:
1. Remove user from `config.yml` 1. Remove user from `config.yml`
2. Restart server 2. Restart server
3. User's connections will be rejected 3. User's connections will be rejected
For immediate revocation: For immediate revocation:
- Remove user from config - Remove user from config
- Restart server - Restart server
- Existing connections are terminated - Existing connections are terminated
@ -354,6 +372,7 @@ Grant temporary access:
4. Restart server 4. Restart server
For automation: For automation:
```bash ```bash
# Add user with expiry comment # Add user with expiry comment
echo " - name: temp-user # EXPIRES: 2024-12-31" >> config.yml echo " - name: temp-user # EXPIRES: 2024-12-31" >> config.yml
@ -363,6 +382,7 @@ echo " token: temp-token" >> config.yml
### Shared Tokens (Not Recommended) ### Shared Tokens (Not Recommended)
Multiple users sharing a token: Multiple users sharing a token:
- All appear as same user in logs - All appear as same user in logs
- Can't revoke individual access - Can't revoke individual access
- Security risk if one person leaves - Security risk if one person leaves
@ -462,6 +482,7 @@ VaultLink doesn't support runtime user management. To change access:
3. Users reconnect automatically 3. Users reconnect automatically
For frequent changes, consider: For frequent changes, consider:
- Over-provision access (deny list) - Over-provision access (deny list)
- Use external authentication proxy - Use external authentication proxy
- Script config updates + reload - Script config updates + reload
@ -471,18 +492,21 @@ For frequent changes, consider:
### Can't connect ### Can't connect
**Check token**: **Check token**:
```bash ```bash
# Verify token in config matches client # Verify token in config matches client
grep "token:" config.yml grep "token:" config.yml
``` ```
**Check vault name**: **Check vault name**:
```bash ```bash
# Ensure vault is in allowed list # Ensure vault is in allowed list
grep -A 5 "name: alice" config.yml grep -A 5 "name: alice" config.yml
``` ```
**Check server logs**: **Check server logs**:
```bash ```bash
tail -f logs/*.log | grep -i auth tail -f logs/*.log | grep -i auth
``` ```
@ -490,6 +514,7 @@ tail -f logs/*.log | grep -i auth
### Access denied ### Access denied
**Verify vault access**: **Verify vault access**:
```yaml ```yaml
# Check user's vault_access configuration # Check user's vault_access configuration
users: users:
@ -502,6 +527,7 @@ users:
``` ```
**Case sensitivity**: **Case sensitivity**:
- Vault names are case-sensitive - Vault names are case-sensitive
- `Vault``vault` - `Vault``vault`
- Ensure exact match in config and client - Ensure exact match in config and client
@ -509,11 +535,13 @@ users:
### Token not working ### Token not working
**Check for typos**: **Check for typos**:
- Extra spaces - Extra spaces
- Hidden characters - Hidden characters
- Wrong quotes in YAML - Wrong quotes in YAML
**Regenerate token**: **Regenerate token**:
```bash ```bash
# Generate new token # Generate new token
openssl rand -hex 32 openssl rand -hex 32

View file

@ -66,6 +66,7 @@ database:
``` ```
The directory structure: The directory structure:
``` ```
databases/ databases/
├── vault-1.db ├── vault-1.db
@ -74,6 +75,7 @@ databases/
``` ```
**Notes**: **Notes**:
- Path is relative to working directory or absolute - Path is relative to working directory or absolute
- Directory must be writable by the server process - Directory must be writable by the server process
- Ensure adequate disk space for vault data - Ensure adequate disk space for vault data
@ -94,6 +96,7 @@ database:
``` ```
**Tuning**: **Tuning**:
- Higher values: Better performance under load - Higher values: Better performance under load
- Lower values: Less memory usage - Lower values: Less memory usage
- Typical range: 8-20 - Typical range: 8-20
@ -114,6 +117,7 @@ database:
``` ```
**Notes**: **Notes**:
- Cursors track client sync state - Cursors track client sync state
- Timeout too short: Clients may need to re-sync frequently - Timeout too short: Clients may need to re-sync frequently
- Timeout too long: More memory usage - Timeout too long: More memory usage
@ -139,6 +143,7 @@ server:
``` ```
**Common values**: **Common values**:
- `0.0.0.0`: Listen on all network interfaces (production) - `0.0.0.0`: Listen on all network interfaces (production)
- `127.0.0.1`: Listen on localhost only (development/testing) - `127.0.0.1`: Listen on localhost only (development/testing)
- Specific IP: Listen on specific interface - Specific IP: Listen on specific interface
@ -158,6 +163,7 @@ server:
``` ```
**Notes**: **Notes**:
- Must be available (not in use) - Must be available (not in use)
- Privileged ports (< 1024) require root - Privileged ports (< 1024) require root
- Common ports: 3000, 8080, 8888 - Common ports: 3000, 8080, 8888
@ -178,12 +184,14 @@ server:
``` ```
**Usage**: **Usage**:
- Limits file upload size - Limits file upload size
- Prevents memory exhaustion attacks - Prevents memory exhaustion attacks
- Must be larger than largest expected file - Must be larger than largest expected file
- Consider client `max_file_size_mb` settings - Consider client `max_file_size_mb` settings
**Tuning**: **Tuning**:
- Small vaults (mostly text): 100 MB - Small vaults (mostly text): 100 MB
- Medium vaults (some images): 512 MB - Medium vaults (some images): 512 MB
- Large vaults (many images/PDFs): 1024+ MB - Large vaults (many images/PDFs): 1024+ MB
@ -203,12 +211,14 @@ server:
``` ```
**Notes**: **Notes**:
- Limits concurrent WebSocket connections - Limits concurrent WebSocket connections
- Prevents resource exhaustion - Prevents resource exhaustion
- Consider expected number of users - Consider expected number of users
- Each client uses memory and file descriptors - Each client uses memory and file descriptors
**Scaling**: **Scaling**:
- Personal use: 10-50 - Personal use: 10-50
- Small team: 50-100 - Small team: 50-100
- Large team: 100-500 - Large team: 100-500
@ -228,11 +238,13 @@ server:
``` ```
**Usage**: **Usage**:
- Timeout for HTTP requests - Timeout for HTTP requests
- Timeout for WebSocket operations - Timeout for WebSocket operations
- Clients disconnected if unresponsive - Clients disconnected if unresponsive
**Tuning**: **Tuning**:
- Fast networks: 30 seconds - Fast networks: 30 seconds
- Slow networks: 90-120 seconds - Slow networks: 90-120 seconds
- Large file uploads: Increase proportionally - Large file uploads: Increase proportionally
@ -259,6 +271,7 @@ logging:
``` ```
**Notes**: **Notes**:
- Path is relative to working directory or absolute - Path is relative to working directory or absolute
- Directory must be writable - Directory must be writable
- Logs are rotated based on `log_rotation` - Logs are rotated based on `log_rotation`
@ -284,10 +297,12 @@ logging:
**Format**: `<number><unit>` **Format**: `<number><unit>`
**Units**: **Units**:
- `hours`: Hours (e.g., `12hours`, `24hours`) - `hours`: Hours (e.g., `12hours`, `24hours`)
- `days`: Days (e.g., `7days`, `30days`) - `days`: Days (e.g., `7days`, `30days`)
**Recommendations**: **Recommendations**:
- Development: `24hours` or `7days` - Development: `24hours` or `7days`
- Production: `7days` or `30days` - Production: `7days` or `30days`
- High traffic: `24hours` (logs can be large) - High traffic: `24hours` (logs can be large)
@ -362,6 +377,7 @@ tail -f logs/latest.log
``` ```
**Common errors**: **Common errors**:
- Missing required fields - Missing required fields
- Invalid YAML syntax - Invalid YAML syntax
- Invalid values (negative numbers, etc.) - Invalid values (negative numbers, etc.)
@ -431,12 +447,14 @@ server:
### Server won't start ### Server won't start
**Check YAML syntax**: **Check YAML syntax**:
```bash ```bash
# Use a YAML validator # Use a YAML validator
python -c 'import yaml, sys; yaml.safe_load(open("config.yml"))' python -c 'import yaml, sys; yaml.safe_load(open("config.yml"))'
``` ```
**Check file paths**: **Check file paths**:
```bash ```bash
# Ensure directories exist and are writable # Ensure directories exist and are writable
mkdir -p databases logs mkdir -p databases logs
@ -444,6 +462,7 @@ chmod 755 databases logs
``` ```
**Check port availability**: **Check port availability**:
```bash ```bash
# Verify port is not in use # Verify port is not in use
lsof -i :3000 lsof -i :3000

324
docs/guide/alternatives.md Normal file
View file

@ -0,0 +1,324 @@
# Comparison with Alternatives
VaultLink is one of several solutions for synchronizing Obsidian vaults. This page compares VaultLink with popular alternatives to help you choose the right tool.
## Key Differentiator: Editor Agnostic
**VaultLink is not tied to Obsidian.** While it includes an Obsidian plugin for convenience, VaultLink synchronizes plain text files and works with any editor:
- Edit with **Obsidian desktop** on your laptop
- Edit with **Vim** on your server
- Edit with **VS Code** on your workstation
- Edit with **Obsidian mobile** on your phone
- Use the **CLI client** for automated workflows
All changes merge automatically without conflict markers, regardless of which editor you use. This is possible because VaultLink uses [reconcile-text](/architecture/sync-algorithm#why-reconcile-text-over-crdts) for differential synchronization rather than requiring operation-level tracking.
## VaultLink's Core Strengths
Before diving into comparisons:
1. **Fully self-hosted**: Server and all components are open source
2. **Collaborative editing**: Real-time sync with operational transformation
3. **Automatic conflict resolution**: No manual intervention or paid features required
4. **Cursor tracking**: See where other users are editing
5. **Extensively tested**: Comprehensive test suite for server and client
6. **Editor freedom**: Use any text editor, not just Obsidian
7. **Production-ready**: Docker images, health checks, monitoring
## Obsidian Sync Alternatives
### Self-hosted LiveSync
**Downloads**: ~300,000
**Repository**: https://github.com/vrtmrz/obsidian-livesync
**Overview**: CouchDB/IBM Cloudant-based sync with end-to-end encryption.
| Aspect | Self-hosted LiveSync | VaultLink |
| ------------------------- | --------------------------- | -------------------------------------- |
| **Self-hosted** | Yes (CouchDB required) | Yes (single binary or Docker) |
| **Conflict resolution** | Manual or automatic (basic) | Automatic (operational transformation) |
| **Collaborative editing** | No | Yes (real-time with cursors) |
| **Editor support** | Obsidian only | Any text editor |
| **Infrastructure** | CouchDB database | SQLite (bundled) |
| **Deployment complexity** | Medium (external DB) | Low (single container) |
| **End-to-end encryption** | Yes | No (transport encryption only) |
| **Out-of-band edits** | Limited support | Full support (edit with any tool) |
**When to use LiveSync**:
- Need end-to-end encryption
- Already running CouchDB
- Only use Obsidian (no external editors)
**When to use VaultLink**:
- Want collaborative editing with multiple users
- Edit files with various tools (Vim, VS Code, etc.)
- Need simpler deployment (no external database)
- Want operational transformation for better merges
---
### Remotely Save
**Downloads**: ~1.1M
**Repository**: https://github.com/remotely-save/remotely-save
**Overview**: Sync to cloud storage providers (S3, Dropbox, OneDrive, WebDAV).
| Aspect | Remotely Save | VaultLink |
| ------------------------- | ---------------------------- | ------------------------ |
| **Self-hosted** | Partial (uses cloud storage) | Fully self-hosted |
| **Conflict resolution** | Paid Pro feature | Free and automatic |
| **Collaborative editing** | No | Yes |
| **Editor support** | Obsidian only | Any text editor |
| **Storage backend** | Cloud providers | Self-hosted SQLite |
| **Cost** | Free (basic) / Paid (Pro) | Free (open source) |
| **Code quality** | No tests, complex codebase | Comprehensive test suite |
| **Real-time sync** | No (periodic polling) | Yes (WebSocket) |
**When to use Remotely Save**:
- Already use cloud storage (S3, Dropbox)
- Don't need real-time sync
- Single-user scenario
**When to use VaultLink**:
- Want full control over data
- Need automatic conflict resolution without paying
- Want real-time collaborative editing
- Value code quality and testing
**Note**: Remotely Save's conflict resolution is a paid feature. VaultLink provides superior automatic merging for free.
---
### Relay
**Downloads**: ~24,000
**Repository**: https://github.com/No-Instructions/Relay
**Overview**: CRDT-based sync with proprietary server component.
| Aspect | Relay | VaultLink |
| -------------------------- | ---------------------------- | ----------------------- |
| **Self-hosted** | No (proprietary server) | Yes (fully open source) |
| **Conflict resolution** | CRDT (automatic) | OT (automatic) |
| **Collaborative editing** | Yes | Yes |
| **Editor support** | Obsidian only | Any text editor |
| **Out-of-band edits** | No (breaks CRDT consistency) | Yes (differential sync) |
| **Server open source** | No | Yes |
| **Infrastructure control** | Limited | Full |
| **Per-file overhead** | High (CRDT metadata) | Low (version history) |
**When to use Relay**:
- Want hosted solution (don't self-host)
- Only edit within Obsidian
- Don't need out-of-band editing
**When to use VaultLink**:
- Need fully open source solution
- Want to self-host completely
- Edit files outside Obsidian (Vim, VS Code)
- Value infrastructure control
**Critical limitation**: Relay's CRDT approach requires tracking every operation within Obsidian. Editing files outside Obsidian breaks the CRDT state. VaultLink's differential sync works regardless of how files are edited.
---
### Obsidian Git
**Downloads**: ~1.4M
**Repository**: https://github.com/denolehov/obsidian-git
**Overview**: Uses Git for version control and synchronization.
| Aspect | Obsidian Git | VaultLink |
| ------------------------- | ----------------------------- | ----------------------- |
| **Self-hosted** | Yes (Git server) | Yes (sync server) |
| **Conflict resolution** | Manual (conflict markers) | Automatic (no markers) |
| **Collaborative editing** | No | Yes (real-time) |
| **Editor support** | Any (it's Git) | Any (differential sync) |
| **Version history** | Full Git history | Document versions |
| **Real-time sync** | No (commit-based) | Yes (instant) |
| **Merge conflicts** | Manual resolution | Automatic |
| **Learning curve** | High (Git knowledge required) | Low |
| **Workflow interruption** | Yes (resolve conflicts) | No |
**When to use Obsidian Git**:
- Need full version control (branches, tags, etc.)
- Already familiar with Git workflows
- Want integration with existing Git repos
- Don't mind manual conflict resolution
**When to use VaultLink**:
- Want automatic conflict-free merging
- Need real-time collaborative editing
- Don't want workflow interruptions from merge conflicts
- Prefer simpler mental model (sync, not commits)
**Key difference**: Git requires manual conflict resolution with `<<<<<<<` markers. VaultLink automatically merges all changes using operational transformation, never interrupting your workflow.
---
### Syncthing Integration
**Downloads**: ~22,600
**Repository**: https://github.com/LBF38/obsidian-syncthing-integration
**Overview**: Wrapper around Syncthing for file synchronization.
| Aspect | Syncthing Integration | VaultLink |
| ------------------------- | ------------------------------ | ----------------- |
| **Self-hosted** | Yes (Syncthing) | Yes (sync server) |
| **Conflict resolution** | Manual | Automatic |
| **Collaborative editing** | No | Yes |
| **Editor support** | Any | Any |
| **Status** | Unfinished | Production-ready |
| **Conflict files** | Creates `.sync-conflict` files | No conflict files |
| **Real-time sync** | Yes | Yes |
| **Automatic merging** | No | Yes |
**When to use Syncthing Integration**:
- Already use Syncthing for other files
- Don't need automatic conflict resolution
- Single-user with multiple devices
**When to use VaultLink**:
- Want automatic conflict resolution
- Need collaborative editing
- Want production-ready solution
- Don't want to manage conflict files
**Status note**: Syncthing Integration is marked as unfinished. VaultLink is production-ready with comprehensive testing.
---
### Remotely Sync
**Downloads**: ~38,000
**Repository**: https://github.com/sboesen/remotely-sync
**Overview**: Similar to Remotely Save, syncs to cloud storage.
| Aspect | Remotely Sync | VaultLink |
| ----------------------- | ----------------------- | ------------------- |
| **Self-hosted** | Partial (cloud storage) | Fully self-hosted |
| **Conflict resolution** | Limited/Paid | Free and automatic |
| **Code quality** | No tests | Comprehensive tests |
| **Maintenance** | Low activity | Active development |
**Same concerns as Remotely Save**: No test suite, conflict resolution limitations, cloud storage dependency.
**When to use VaultLink**: See Remotely Save comparison above.
---
### SyncFTP
**Downloads**: ~5,000
**Repository**: https://github.com/alex-donnan/SyncFTP
**Overview**: Simple FTP-based file synchronization.
| Aspect | SyncFTP | VaultLink |
| ------------------------- | ---------------------- | ---------------- |
| **Conflict resolution** | None (last write wins) | Automatic (OT) |
| **Data loss risk** | High (overwrites) | None (merges) |
| **Collaborative editing** | No | Yes |
| **Sophistication** | Minimal | Production-grade |
**When to use SyncFTP**: Don't use SyncFTP for any scenario where data integrity matters.
**When to use VaultLink**: Any scenario requiring reliable synchronization.
---
## Feature Comparison Matrix
| Feature | VaultLink | LiveSync | Relay | Git | Remotely Save | Syncthing |
| --------------------------------- | --------- | -------- | ----- | --- | ------------- | --------- |
| **Fully open source** | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
| **Self-hosted** | ✅ | ✅ | ❌ | ✅ | Partial | ✅ |
| **Automatic conflict resolution** | ✅ | Basic | ✅ | ❌ | Paid | ❌ |
| **Real-time sync** | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
| **Collaborative editing** | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| **Cursor tracking** | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **Editor agnostic** | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ |
| **Out-of-band edits** | ✅ | Limited | ❌ | ✅ | ❌ | ✅ |
| **No conflict markers** | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| **Comprehensive tests** | ✅ | ❌ | ❌ | N/A | ❌ | N/A |
| **Simple deployment** | ✅ | ❌ | N/A | ❌ | ✅ | ❌ |
| **Low infrastructure** | ✅ | ❌ | N/A | ✅ | ✅ | ✅ |
---
## VaultLink's Unique Position
VaultLink is the **only** solution that combines:
1. **Fully open source** self-hosted server
2. **Editor agnostic** operation (not locked to Obsidian)
3. **Automatic conflict-free merging** using operational transformation
4. **Real-time collaborative editing** with cursor tracking
5. **Differential synchronization** supporting out-of-band edits
6. **Comprehensive test coverage** ensuring reliability
7. **Simple deployment** via Docker or single binary
## Use Case Recommendations
### Choose VaultLink when you:
- Edit vaults with multiple editors (Obsidian + Vim + VS Code)
- Need real-time collaboration with teammates
- Want automatic conflict resolution without manual intervention
- Value full control over infrastructure
- Need production-ready reliability with comprehensive testing
- Want to edit files while offline and sync later seamlessly
### Consider alternatives when you:
- **LiveSync**: Need end-to-end encryption and only use Obsidian
- **Git**: Need full version control with branches and advanced Git features
- **Remotely Save**: Already committed to cloud storage providers
- **Syncthing**: Already use Syncthing and don't need automatic merging
## Migration from Other Solutions
VaultLink works with plain Markdown files, making migration simple:
1. **From Git**: Clone your repo, point VaultLink to the directory
2. **From cloud sync**: Download files, configure VaultLink client
3. **From LiveSync**: Export vault, import to VaultLink
4. **From Syncthing**: Point VaultLink to synced directory
All solutions work with the same Markdown files—VaultLink just syncs them better.
## Beyond Obsidian
Because VaultLink is editor-agnostic, you can use it for:
- **Documentation teams**: Sync technical docs edited in VS Code
- **Academic writing**: Collaborate on papers with various Markdown editors
- **Personal knowledge bases**: Use Obsidian on mobile, Vim on servers
- **Automated workflows**: CLI client for backup systems and CI/CD
- **Multi-tool workflows**: Different team members use different editors
VaultLink doesn't lock you into Obsidian—it's a general-purpose differential sync system that happens to work excellently with Obsidian vaults.
## Next Steps
Ready to try VaultLink?
- [Get started →](/guide/getting-started)
- [Understand the architecture →](/architecture/)
- [See how sync works →](/architecture/sync-algorithm)

View file

@ -94,7 +94,7 @@ docker compose up -d
### Required Arguments ### Required Arguments
| Argument | Short | Description | Example | | Argument | Short | Description | Example |
|----------|-------|-------------|---------| | -------------- | ----- | ----------------------- | ------------------------ |
| `--local-path` | `-l` | Local directory to sync | `/vault` | | `--local-path` | `-l` | Local directory to sync | `/vault` |
| `--remote-uri` | `-r` | Server WebSocket URI | `wss://sync.example.com` | | `--remote-uri` | `-r` | Server WebSocket URI | `wss://sync.example.com` |
| `--token` | `-t` | Authentication token | `abc123...` | | `--token` | `-t` | Authentication token | `abc123...` |
@ -103,7 +103,7 @@ docker compose up -d
### Optional Arguments ### Optional Arguments
| Argument | Default | Description | | Argument | Default | Description |
|----------|---------|-------------| | ------------------------------- | ------- | -------------------------------------- |
| `--sync-concurrency` | `1` | Concurrent file operations | | `--sync-concurrency` | `1` | Concurrent file operations |
| `--max-file-size-mb` | `10` | Max file size in MB | | `--max-file-size-mb` | `10` | Max file size in MB |
| `--ignore-pattern` | - | Glob pattern to ignore (repeatable) | | `--ignore-pattern` | - | Glob pattern to ignore (repeatable) |
@ -228,6 +228,7 @@ docker inspect --format='{{json .State.Health}}' vaultlink-sync | jq
``` ```
Health check verifies: Health check verifies:
- Health file exists - Health file exists
- Status updated within last 30 seconds - Status updated within last 30 seconds
- WebSocket connection is active - WebSocket connection is active
@ -351,21 +352,25 @@ services:
### Client won't connect ### Client won't connect
**Check server accessibility**: **Check server accessibility**:
```bash ```bash
curl https://sync.example.com/vaults/test/ping curl https://sync.example.com/vaults/test/ping
``` ```
**Verify WebSocket protocol**: **Verify WebSocket protocol**:
- Use `ws://` for HTTP servers - Use `ws://` for HTTP servers
- Use `wss://` for HTTPS servers - Use `wss://` for HTTPS servers
**Check authentication**: **Check authentication**:
- Token must match server config - Token must match server config
- User must have access to the vault - User must have access to the vault
### Permission errors ### Permission errors
**Docker volume permissions**: **Docker volume permissions**:
```bash ```bash
# Ensure directory is writable # Ensure directory is writable
chmod 755 /path/to/vault chmod 755 /path/to/vault
@ -375,6 +380,7 @@ docker run --rm ghcr.io/schmelczer/vault-link-cli:latest id
``` ```
**SELinux issues**: **SELinux issues**:
```bash ```bash
# Add :z flag to volume mount # Add :z flag to volume mount
docker run -v /path/to/vault:/vault:z ... docker run -v /path/to/vault:/vault:z ...
@ -383,14 +389,17 @@ docker run -v /path/to/vault:/vault:z ...
### Files not syncing ### Files not syncing
**Check ignore patterns**: **Check ignore patterns**:
- View logs to see which files are skipped - View logs to see which files are skipped
- Ensure patterns don't match unintentionally - Ensure patterns don't match unintentionally
**File size limits**: **File size limits**:
- Check `--max-file-size-mb` setting - Check `--max-file-size-mb` setting
- Large files are skipped with a warning - Large files are skipped with a warning
**Check metadata**: **Check metadata**:
```bash ```bash
# View sync metadata # View sync metadata
cat /path/to/vault/.vaultlink/metadata.json cat /path/to/vault/.vaultlink/metadata.json
@ -399,33 +408,39 @@ cat /path/to/vault/.vaultlink/metadata.json
### High memory usage ### High memory usage
**Reduce concurrency**: **Reduce concurrency**:
```bash ```bash
--sync-concurrency 1 --sync-concurrency 1
``` ```
**Limit file sizes**: **Limit file sizes**:
```bash ```bash
--max-file-size-mb 5 --max-file-size-mb 5
``` ```
**Check vault size**: **Check vault size**:
- Very large vaults may need more resources - Very large vaults may need more resources
- Consider splitting into multiple vaults - Consider splitting into multiple vaults
### Connection keeps dropping ### Connection keeps dropping
**Increase retry interval**: **Increase retry interval**:
```bash ```bash
--websocket-retry-interval-ms 5000 --websocket-retry-interval-ms 5000
``` ```
**Check network stability**: **Check network stability**:
```bash ```bash
# Monitor connection # Monitor connection
docker logs -f vaultlink-sync | grep -i websocket docker logs -f vaultlink-sync | grep -i websocket
``` ```
**Server timeout settings**: **Server timeout settings**:
- Verify reverse proxy WebSocket timeout - Verify reverse proxy WebSocket timeout
- Check server `response_timeout_seconds` - Check server `response_timeout_seconds`
@ -503,6 +518,7 @@ WantedBy=multi-user.target
``` ```
Enable and start: Enable and start:
```bash ```bash
sudo systemctl daemon-reload sudo systemctl daemon-reload
sudo systemctl enable vaultlink-cli sudo systemctl enable vaultlink-cli

View file

@ -159,11 +159,13 @@ Want to understand how VaultLink works under the hood?
### Server won't start ### Server won't start
Check Docker logs: Check Docker logs:
```bash ```bash
docker logs vaultlink-server docker logs vaultlink-server
``` ```
Common issues: Common issues:
- Port 3000 already in use: Change the port mapping `-p 3001:3000` - Port 3000 already in use: Change the port mapping `-p 3001:3000`
- Config file errors: Validate YAML syntax - Config file errors: Validate YAML syntax
- Permission issues: Ensure the volume mount is writable - Permission issues: Ensure the volume mount is writable

View file

@ -27,6 +27,7 @@ After installation, configure the plugin in **Settings → VaultLink**.
### Required Settings ### Required Settings
#### Server URL #### Server URL
The WebSocket URL of your sync server. The WebSocket URL of your sync server.
- **Development/Local**: `ws://localhost:3000` - **Development/Local**: `ws://localhost:3000`
@ -37,14 +38,17 @@ Use `ws://` for unencrypted connections and `wss://` for SSL connections (produc
::: :::
#### Authentication Token #### Authentication Token
Your authentication token from the server's `config.yml`. Your authentication token from the server's `config.yml`.
Generate a secure token: Generate a secure token:
```bash ```bash
openssl rand -hex 32 openssl rand -hex 32
``` ```
#### Vault Name #### Vault Name
The name of the vault on the server. Can be any string. The name of the vault on the server. Can be any string.
Multiple Obsidian vaults can sync to the same server vault name (for shared vaults), or use unique names for separate vaults. Multiple Obsidian vaults can sync to the same server vault name (for shared vaults), or use unique names for separate vaults.
@ -52,26 +56,34 @@ Multiple Obsidian vaults can sync to the same server vault name (for shared vaul
### Optional Settings ### Optional Settings
#### Sync Concurrency #### Sync Concurrency
Number of files to sync simultaneously. Number of files to sync simultaneously.
- **Default**: 1 - **Default**: 1
- **Range**: 1-10 - **Range**: 1-10
- Higher values = faster initial sync, more resource usage - Higher values = faster initial sync, more resource usage
#### Max File Size #### Max File Size
Maximum file size to sync (in MB). Maximum file size to sync (in MB).
- **Default**: 10 - **Default**: 10
- Files larger than this are skipped - Files larger than this are skipped
#### Ignore Patterns #### Ignore Patterns
Glob patterns for files to exclude from sync. Glob patterns for files to exclude from sync.
Examples: Examples:
- `*.tmp` - Ignore temporary files - `*.tmp` - Ignore temporary files
- `.trash/**` - Ignore trash folder - `.trash/**` - Ignore trash folder
- `private/**` - Ignore private directory - `private/**` - Ignore private directory
#### WebSocket Retry Interval #### WebSocket Retry Interval
Milliseconds between reconnection attempts when disconnected. Milliseconds between reconnection attempts when disconnected.
- **Default**: 3500ms - **Default**: 3500ms
- Increase for flaky networks to avoid connection spam - Increase for flaky networks to avoid connection spam
@ -172,9 +184,11 @@ Share specific folders while keeping others private:
### Plugin won't connect ### Plugin won't connect
1. **Verify server is running**: 1. **Verify server is running**:
```bash ```bash
curl http://your-server:3000/vaults/test/ping curl http://your-server:3000/vaults/test/ping
``` ```
Should return `pong` Should return `pong`
2. **Check URL format**: 2. **Check URL format**:

View file

@ -76,6 +76,7 @@ chmod +x sync_server-linux-x86_64
### Build from Source ### Build from Source
Requirements: Requirements:
- Rust 1.89.0 or later - Rust 1.89.0 or later
- SQLite development headers - SQLite development headers
- SQLx CLI - SQLx CLI
@ -192,6 +193,7 @@ server {
``` ```
Reload Nginx: Reload Nginx:
```bash ```bash
sudo nginx -t sudo nginx -t
sudo systemctl reload nginx sudo systemctl reload nginx
@ -208,6 +210,7 @@ sync.example.com {
``` ```
Start Caddy: Start Caddy:
```bash ```bash
caddy run --config Caddyfile caddy run --config Caddyfile
``` ```
@ -269,6 +272,7 @@ find /backup/vaultlink -type d -mtime +30 -exec rm -rf {} +
``` ```
Run daily via cron: Run daily via cron:
```cron ```cron
0 2 * * * /opt/vaultlink/backup.sh 0 2 * * * /opt/vaultlink/backup.sh
``` ```
@ -293,12 +297,14 @@ For advanced monitoring, collect Docker stats or implement custom metrics.
#### Log Monitoring #### Log Monitoring
Logs are written to the configured `log_directory`. Monitor for: Logs are written to the configured `log_directory`. Monitor for:
- Connection failures - Connection failures
- Authentication errors - Authentication errors
- Database errors - Database errors
- WebSocket disconnections - WebSocket disconnections
Example log watching: Example log watching:
```bash ```bash
tail -f /data/logs/*.log | grep -i error tail -f /data/logs/*.log | grep -i error
``` ```
@ -316,11 +322,13 @@ VaultLink currently uses SQLite, which limits horizontal scaling. For multiple s
### Vertical Scaling ### Vertical Scaling
Increase resources for the server: Increase resources for the server:
- More CPU for handling concurrent connections - More CPU for handling concurrent connections
- More RAM for database caching - More RAM for database caching
- Faster storage (SSD) for database operations - Faster storage (SSD) for database operations
Tune configuration: Tune configuration:
- Increase `max_clients_per_vault` for more concurrent users - Increase `max_clients_per_vault` for more concurrent users
- Increase `max_connections_per_vault` for database performance - Increase `max_connections_per_vault` for database performance
- Adjust `max_body_size_mb` based on typical file sizes - Adjust `max_body_size_mb` based on typical file sizes

View file

@ -9,6 +9,7 @@ VaultLink consists of three main components:
### Sync Server ### Sync Server
A Rust-based WebSocket server that handles: A Rust-based WebSocket server that handles:
- Real-time bidirectional synchronization - Real-time bidirectional synchronization
- Document versioning with SQLite - Document versioning with SQLite
- User authentication and vault access control - User authentication and vault access control
@ -17,6 +18,7 @@ A Rust-based WebSocket server that handles:
### Obsidian Plugin ### Obsidian Plugin
A native Obsidian plugin that: A native Obsidian plugin that:
- Integrates sync directly into your Obsidian workflow - Integrates sync directly into your Obsidian workflow
- Provides real-time updates as you edit - Provides real-time updates as you edit
- Handles file watching and automatic synchronization - Handles file watching and automatic synchronization
@ -25,6 +27,7 @@ A native Obsidian plugin that:
### CLI Client ### CLI Client
A standalone synchronization client that: A standalone synchronization client that:
- Syncs vaults without requiring Obsidian - Syncs vaults without requiring Obsidian
- Perfect for servers, automation, or backup systems - Perfect for servers, automation, or backup systems
- Provides file watching and bidirectional sync - Provides file watching and bidirectional sync
@ -39,6 +42,7 @@ Changes are synchronized immediately via WebSocket connections. When multiple us
### Self-Hosted Architecture ### Self-Hosted Architecture
Run the sync server on your own infrastructure: Run the sync server on your own infrastructure:
- Full control over data storage and access - Full control over data storage and access
- No dependency on third-party services - No dependency on third-party services
- Configurable authentication and authorization - Configurable authentication and authorization
@ -47,6 +51,7 @@ Run the sync server on your own infrastructure:
### Operational Transformation ### Operational Transformation
VaultLink uses the `reconcile-text` library for intelligent conflict resolution: VaultLink uses the `reconcile-text` library for intelligent conflict resolution:
- Simultaneous edits are automatically merged - Simultaneous edits are automatically merged
- No manual conflict resolution required - No manual conflict resolution required
- Preserves intent of all contributors - Preserves intent of all contributors
@ -55,6 +60,7 @@ VaultLink uses the `reconcile-text` library for intelligent conflict resolution:
### Flexible Authentication ### Flexible Authentication
Configure user access per vault: Configure user access per vault:
- Token-based authentication - Token-based authentication
- Per-user vault access control - Per-user vault access control
- Allow-list or deny-list patterns - Allow-list or deny-list patterns
@ -65,6 +71,7 @@ Configure user access per vault:
### Personal Sync ### Personal Sync
Synchronize your Obsidian vault across multiple devices: Synchronize your Obsidian vault across multiple devices:
- Laptop, desktop, and mobile in real-time - Laptop, desktop, and mobile in real-time
- No cloud service subscription required - No cloud service subscription required
- Full privacy and data control - Full privacy and data control
@ -72,6 +79,7 @@ Synchronize your Obsidian vault across multiple devices:
### Team Collaboration ### Team Collaboration
Share knowledge bases with teammates: Share knowledge bases with teammates:
- Real-time collaborative editing - Real-time collaborative editing
- Granular access control per vault - Granular access control per vault
- Self-hosted for enterprise security requirements - Self-hosted for enterprise security requirements
@ -79,6 +87,7 @@ Share knowledge bases with teammates:
### Automated Backups ### Automated Backups
Use the CLI client for automated workflows: Use the CLI client for automated workflows:
- Scheduled backups to remote servers - Scheduled backups to remote servers
- Integration with existing backup systems - Integration with existing backup systems
- Headless operation without Obsidian - Headless operation without Obsidian
@ -86,6 +95,7 @@ Use the CLI client for automated workflows:
### Development & Testing ### Development & Testing
Synchronize documentation across environments: Synchronize documentation across environments:
- Keep docs in sync with development environments - Keep docs in sync with development environments
- Automated deployment of documentation - Automated deployment of documentation
- Version control integration - Version control integration

View file

@ -6,12 +6,15 @@
"scripts": { "scripts": {
"dev": "vitepress dev", "dev": "vitepress dev",
"build": "vitepress build", "build": "vitepress build",
"preview": "vitepress preview" "preview": "vitepress preview",
"format": "prettier --write \"**/*.md\" \"**/*.mts\"",
"format:check": "prettier --check \"**/*.md\" \"**/*.mts\""
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"prettier": "^3.6.2",
"vitepress": "^1.6.4", "vitepress": "^1.6.4",
"vue": "^3.5.24" "vue": "^3.5.24"
} }

View file

@ -1,34 +1,47 @@
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"> <svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#4A90E2;stop-opacity:1" />
<stop offset="100%" style="stop-color:#357ABD;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background circle --> <!-- Background circle -->
<circle cx="100" cy="100" r="95" fill="#4A90E2" opacity="0.1"/> <circle cx="100" cy="100" r="90" fill="url(#grad1)" opacity="0.15"/>
<!-- Link chain symbol --> <!-- Main vault icon -->
<g transform="translate(100, 100)"> <g transform="translate(100, 100)">
<!-- Vault body -->
<rect x="-45" y="-50" width="90" height="80" rx="8" fill="none" stroke="url(#grad1)" stroke-width="6"/>
<!-- Vault door circle -->
<circle cx="0" cy="-10" r="22" fill="none" stroke="url(#grad1)" stroke-width="5"/>
<circle cx="0" cy="-10" r="14" fill="none" stroke="url(#grad1)" stroke-width="3"/>
<circle cx="0" cy="-10" r="6" fill="url(#grad1)"/>
<!-- Vault handle -->
<line x1="0" y1="-4" x2="18" y2="-4" stroke="url(#grad1)" stroke-width="3" stroke-linecap="round"/>
<circle cx="18" cy="-4" r="4" fill="url(#grad1)"/>
<!-- Link chain -->
<g opacity="0.9">
<!-- Left link --> <!-- Left link -->
<path d="M -60 -10 L -30 -10 C -20 -10 -15 -5 -15 5 L -15 5 C -15 15 -20 20 -30 20 L -60 20 C -70 20 -75 15 -75 5 L -75 -5 C -75 -15 -70 -20 -60 -20 Z" <ellipse cx="-30" cy="40" rx="12" ry="8" fill="none" stroke="url(#grad1)" stroke-width="4"/>
fill="none" stroke="#4A90E2" stroke-width="8" stroke-linecap="round"/>
<!-- Right link --> <!-- Right link -->
<path d="M 60 -10 L 30 -10 C 20 -10 15 -5 15 5 L 15 5 C 15 15 20 20 30 20 L 60 20 C 70 20 75 15 75 5 L 75 -5 C 75 -15 70 -20 60 -20 Z" <ellipse cx="30" cy="40" rx="12" ry="8" fill="none" stroke="url(#grad1)" stroke-width="4"/>
fill="none" stroke="#4A90E2" stroke-width="8" stroke-linecap="round"/> <!-- Center link connecting them -->
<ellipse cx="0" cy="40" rx="12" ry="8" fill="none" stroke="url(#grad1)" stroke-width="4"/>
<!-- Center connecting bar -->
<rect x="-15" y="-6" width="30" height="12" rx="6" fill="#4A90E2"/>
<!-- Vault door detail -->
<circle cx="0" cy="0" r="12" fill="none" stroke="#4A90E2" stroke-width="3"/>
<circle cx="0" cy="0" r="6" fill="#4A90E2"/>
<!-- Sync arrows -->
<g opacity="0.6">
<!-- Top arrow -->
<path d="M -5 -50 L 5 -50 L 0 -40 Z" fill="#4A90E2"/>
<!-- Bottom arrow -->
<path d="M 5 50 L -5 50 L 0 40 Z" fill="#4A90E2"/>
</g>
</g> </g>
<!-- Text (optional) --> <!-- Sync arrows (subtle) -->
<text x="100" y="175" font-family="Arial, sans-serif" font-size="24" font-weight="bold" <g opacity="0.5">
text-anchor="middle" fill="#4A90E2">VaultLink</text> <!-- Clockwise arrow top-right -->
<path d="M 35 -35 Q 50 -35 50 -20 L 50 -15" fill="none" stroke="url(#grad1)" stroke-width="2.5" stroke-linecap="round"/>
<polygon points="50,-15 47,-22 53,-22" fill="url(#grad1)"/>
<!-- Counter-clockwise arrow bottom-left -->
<path d="M -35 25 Q -50 25 -50 10 L -50 5" fill="none" stroke="url(#grad1)" stroke-width="2.5" stroke-linecap="round"/>
<polygon points="-50,5 -47,12 -53,12" fill="url(#grad1)"/>
</g>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2 KiB

Before After
Before After