trilium/COLLABORATIVE_ARCHITECTURE.md
Somoru 08f8a6c7ee feat: implement collaborative multi-user support with permission-aware sync
- Add database migration v234 for collaborative multi-user schema
- Implement permission system with granular access control (read/write/admin)
- Add group management for organizing users
- Implement permission-aware sync filtering (pull and push)
- Add automatic note ownership tracking via CLS
- Create 14 RESTful API endpoints for permissions and groups
- Update authentication for multi-user login
- Maintain backward compatibility with single-user mode
- Add comprehensive documentation

Addresses PR #7441 critical sync blocker issue.
All backend functionality complete and production-ready.
2025-10-22 21:28:22 +05:30

9.7 KiB

Collaborative Multi-User Architecture

Overview

This implementation provides a collaborative multi-user system where users can:

  • Share notes with other users or groups
  • Set granular permissions (read, write, admin) on notes
  • Sync only notes they have access to
  • Collaborate on shared notes in real-time

Architecture Design

Database Schema

1. users table

Stores user accounts for authentication.

CREATE TABLE users (
    userId INTEGER PRIMARY KEY AUTOINCREMENT,
    username TEXT NOT NULL UNIQUE,
    email TEXT,
    passwordHash TEXT NOT NULL,
    salt TEXT NOT NULL,
    role TEXT DEFAULT 'user' CHECK(role IN ('admin', 'user')),
    isActive INTEGER DEFAULT 1,
    utcDateCreated TEXT NOT NULL,
    utcDateModified TEXT NOT NULL,
    lastLoginAt TEXT
)

2. groups table

Allows organizing users into groups for easier permission management.

CREATE TABLE groups (
    groupId INTEGER PRIMARY KEY AUTOINCREMENT,
    groupName TEXT NOT NULL UNIQUE,
    description TEXT,
    createdBy INTEGER NOT NULL,
    utcDateCreated TEXT NOT NULL,
    utcDateModified TEXT NOT NULL,
    FOREIGN KEY (createdBy) REFERENCES users(userId)
)

3. group_members table

Many-to-many relationship between users and groups.

CREATE TABLE group_members (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    groupId INTEGER NOT NULL,
    userId INTEGER NOT NULL,
    addedBy INTEGER NOT NULL,
    utcDateAdded TEXT NOT NULL,
    UNIQUE(groupId, userId),
    FOREIGN KEY (groupId) REFERENCES groups(groupId),
    FOREIGN KEY (userId) REFERENCES users(userId)
)

4. note_ownership table

Tracks the owner/creator of each note.

CREATE TABLE note_ownership (
    noteId TEXT PRIMARY KEY,
    ownerId INTEGER NOT NULL,
    utcDateCreated TEXT NOT NULL,
    FOREIGN KEY (noteId) REFERENCES notes(noteId),
    FOREIGN KEY (ownerId) REFERENCES users(userId)
)

5. note_permissions table

Granular access control for notes.

CREATE TABLE note_permissions (
    permissionId INTEGER PRIMARY KEY AUTOINCREMENT,
    noteId TEXT NOT NULL,
    granteeType TEXT NOT NULL CHECK(granteeType IN ('user', 'group')),
    granteeId INTEGER NOT NULL,
    permission TEXT NOT NULL CHECK(permission IN ('read', 'write', 'admin')),
    grantedBy INTEGER NOT NULL,
    utcDateGranted TEXT NOT NULL,
    utcDateModified TEXT NOT NULL,
    UNIQUE(noteId, granteeType, granteeId),
    FOREIGN KEY (noteId) REFERENCES notes(noteId),
    FOREIGN KEY (grantedBy) REFERENCES users(userId)
)

Permission Model

Permission Levels

  1. read: Can view note and its content
  2. write: Can edit note content and attributes (includes read)
  3. admin: Can edit, delete, and share note with others (includes write + read)

Permission Resolution Rules

  1. Owner: Note owner has implicit admin permission
  2. Direct vs Group: Direct user permissions override group permissions
  3. Highest Wins: If user has multiple permissions (through different groups), the highest level applies
  4. Inheritance: Users inherit permissions from all groups they belong to

Permission Checks

// Check if user can read a note
permissions.checkNoteAccess(userId, noteId, 'read')

// Check if user can edit a note
permissions.checkNoteAccess(userId, noteId, 'write')

// Check if user can share/delete a note
permissions.checkNoteAccess(userId, noteId, 'admin')

Services

1. permissions.ts

Core permission checking and management.

Key Functions:

  • checkNoteAccess(userId, noteId, permission) - Check if user has required permission
  • getUserAccessibleNotes(userId) - Get all notes user can access
  • getUserNotePermissions(userId) - Get permission map for sync filtering
  • grantPermission(noteId, granteeType, granteeId, permission, grantedBy) - Share a note
  • revokePermission(noteId, granteeType, granteeId) - Unshare a note
  • filterEntityChangesForUser(userId, entityChanges) - Filter sync data by permissions

2. group_management.ts

Group creation and membership management.

Key Functions:

  • createGroup(groupName, description, createdBy) - Create new group
  • addUserToGroup(groupId, userId, addedBy) - Add user to group
  • removeUserFromGroup(groupId, userId) - Remove user from group
  • getGroupWithMembers(groupId) - Get group details with member list
  • getUserGroups(userId) - Get all groups a user belongs to

3. user_management_collaborative.ts

User authentication and account management.

Key Functions:

  • createUser(username, password, email, role) - Create new user account
  • validateCredentials(username, password) - Authenticate user login
  • changePassword(userId, newPassword) - Update user password
  • getAllUsers() - List all users
  • isAdmin(userId) - Check if user is admin

Sync Integration

Permission-Aware Sync

The sync mechanism is modified to filter entity changes based on user permissions:

// In sync route (routes/api/sync.ts)
const userId = req.session.userId; // From authenticated session
const accessibleNotes = permissions.getUserAccessibleNotes(userId);

// Filter entity changes
const filteredChanges = entityChanges.filter(ec => {
    if (ec.entityName === 'notes') {
        return accessibleNotes.includes(ec.entityId);
    }
    if (ec.entityName === 'branches' || ec.entityName === 'attributes') {
        // Check if related note is accessible
        const noteId = getNoteIdForEntity(ec);
        return noteId && accessibleNotes.includes(noteId);
    }
    return true; // Allow non-note entities
});

Sync Flow

  1. Pull Changes (Server → Client)

    • Server queries entity_changes table
    • Filters changes by user's accessible notes
    • Returns only changes for notes user has permission to access
  2. Push Changes (Client → Server)

    • Client sends entity changes
    • Server validates user has write/admin permission
    • Rejects changes to notes user doesn't have access to
    • Applies valid changes to database

API Routes

User Management

  • POST /api/users - Create new user (admin only)
  • GET /api/users - List all users (admin only)
  • GET /api/users/:userId - Get user details
  • PUT /api/users/:userId - Update user
  • DELETE /api/users/:userId - Delete user (admin only)
  • POST /api/users/:userId/change-password - Change password

Group Management

  • POST /api/groups - Create new group
  • GET /api/groups - List all groups
  • GET /api/groups/:groupId - Get group with members
  • PUT /api/groups/:groupId - Update group
  • DELETE /api/groups/:groupId - Delete group
  • POST /api/groups/:groupId/members - Add user to group
  • DELETE /api/groups/:groupId/members/:userId - Remove user from group

Permission Management

  • GET /api/notes/:noteId/permissions - Get note permissions
  • POST /api/notes/:noteId/share - Share note with user/group
  • DELETE /api/notes/:noteId/permissions/:permissionId - Revoke permission
  • GET /api/notes/accessible - Get all accessible notes for current user

Usage Examples

Sharing a Note

// Alice (userId=1) shares "Project A" note with Bob (userId=2) with write permission
permissions.grantPermission('projectANoteId', 'user', 2, 'write', 1);

// Alice shares "Project A" with "Team Alpha" group (groupId=5) with read permission
permissions.grantPermission('projectANoteId', 'group', 5, 'read', 1);

Checking Access

// Check if Bob can edit the note
const canEdit = permissions.checkNoteAccess(2, 'projectANoteId', 'write'); // true

// Check if member of Team Alpha can edit (they have read permission)
const canMemberEdit = permissions.checkNoteAccess(3, 'projectANoteId', 'write'); // false

Syncing as User

// Bob syncs his local instance
// Server automatically filters to only send notes Bob has access to:
// - Notes Bob owns
// - Notes explicitly shared with Bob
// - Notes shared with groups Bob belongs to

Security Considerations

  1. Password Security: Uses scrypt with secure parameters for password hashing
  2. Timing Attack Protection: Uses timingSafeEqual for password comparison
  3. SQL Injection: All queries use parameterized statements
  4. Session Management: Requires authenticated session for all operations
  5. Permission Checks: Every operation validates user permissions
  6. Admin Operations: Critical operations (user management) require admin role

Migration from Isolated Model

The previous implementation used isolated users (each user had their own separate notes). This has been completely replaced with the collaborative model:

Old Approach (Isolated):

  • Each user had their own copy of all data
  • No sharing between users
  • Sync didn't work between users

New Approach (Collaborative):

  • Single database with all notes
  • Users share specific notes via permissions
  • Sync works across all users with permission filtering
  • Owner-based access control

Default Configuration

  • Default Admin: username=admin, password=admin123 (must be changed on first login)
  • Default Group: "All Users" group automatically created
  • Existing Notes: All existing notes owned by userId=1 (admin)

Future Enhancements

  1. Permission Inheritance: Inherit permissions from parent notes
  2. Audit Logging: Track who accessed/modified what
  3. Notification System: Notify users when notes are shared with them
  4. Collaborative Editing: Real-time collaborative editing with conflict resolution
  5. Advanced Permissions: Add custom permission levels, time-limited access
  6. API Keys: Per-user API keys for programmatic access

Testing

See comprehensive test suite in /apps/server/src/test/collaborative_multi_user.test.ts for:

  • Permission resolution
  • Sync filtering
  • Group management
  • Edge cases and security