docs/Security/PerUserDataAudit2025-12-23/PERSISTENCE_AUDIT.md
This document audits the persistence mechanisms for Wekan board data, including swimlanes, lists, cards, checklists, and their properties (order, color, background, titles, etc.), as well as per-user settings.
Collection: swimlanes (models/swimlanes.js)
Persisted Fields:
title - Swimlane title (via rename() mutation)sort - Swimlane ordering/position (decimal number)color - Swimlane color (via setColor() mutation)collapsed - Swimlane collapsed state (via collapse() mutation) ⚠️ See note belowarchived - Swimlane archived statusPersistence Mechanism:
Swimlanes.update() and Swimlanes.direct.update()updatedAt, modifiedAt fieldsIssues Found:
collapsed field in swimlanes.js line 127 is set to defaultValue: false but the isCollapsed() helper (line 251-263) checks for per-user stored values. This creates a mismatch between board-level and per-user storage.Collection: lists (models/lists.js)
Persisted Fields:
title - List titlesort - List ordering/position (decimal number)color - List colorcollapsed - List collapsed state (board-wide via REST API line 768-775) ⚠️ See note belowstarred - List starred statuswipLimit - WIP limit configurationarchived - List archived statusPersistence Mechanism:
Lists.update() and Lists.direct.update()updatedAt, modifiedAtIssues Found:
collapsed field (line 147) defaults to false but the isCollapsed() helper (line 303-311) also checks for per-user stored values. The REST API allows board-level collapsed state updates (line 768-775), but client also stores per-user via getCollapsedListFromStorage().swimlaneId field is part of the list (line 48), but draggableLists() method (line 275) filters by board only, suggesting lists are shared across swimlanes rather than per-swimlane.Collection: cards (models/cards.js)
Persisted Fields:
title - Card titlesort - Card ordering/position within listcolor - Card color (via setColor() mutation, line 2268)boardId, swimlaneId, listId - Card locationarchived - Card archived statusdescription - Card descriptionPersistence Mechanism:
move() method (line 2063+) handles reordering and moving cards across swimlanes/lists/boardsmodifiedAt, dateLastActivityIssues Found:
Collection: checklists (models/checklists.js)
Persisted Fields:
title - Checklist title (via setTitle() mutation)sort - Checklist ordering (decimal number)hideCheckedChecklistItems - Toggle for hiding checked itemshideAllChecklistItems - Toggle for hiding all itemsPersistence Mechanism:
Checklists.update()createdAt, modifiedAtCollection: checklistItems (models/checklistItems.js)
Persisted Fields:
title - Item text (via setTitle() mutation)sort - Item ordering within checklist (decimal number)isFinished - Item completion status (via check(), uncheck(), toggleItem() mutations)Persistence Mechanism:
move() mutation (line 159-168) handles reordering within checklistsChecklistItems.update()createdAt, modifiedAtIssue Found:
Collection: positionHistory (models/positionHistory.js)
Purpose: Tracks original positions of swimlanes, lists, and cards before changes
Features:
sort positionImplementation Notes:
hasMoved() and hasMovedFromOriginalPosition() helpersStorage: User profile subdocuments (models/users.js)
profile.listWidths (line 527)listWidths[boardId][listId] = widthsetListWidth() mutation (line 1834)getListWidth(), getListWidthFromStorage() (line 1288-1313)profile.listConstraintsprofile.swimlaneHeights (searchable in line 1047+)swimlaneHeights[boardId][swimlaneId] = heightsetSwimlaneHeight() mutation (line 1878)getSwimlaneHeight(), getSwimlaneHeightFromStorage() (line 1050-1080)profile.collapsedSwimlanes (line 1900)collapsedSwimlanes[boardId][swimlaneId] = booleansetCollapsedSwimlane() mutation (line 1900-1906)getCollapsedSwimlaneFromStorage() (swimlanes.js line 251-263)Users.getPublicCollapsedSwimlane() for public/non-logged-in users (users.js line 60-73)profile.collapsedLists (line 1893)collapsedLists[boardId][listId] = booleansetCollapsedList() mutation (line 1893-1899)getCollapsedListFromStorage() (lists.js line 303-311)Users.getPublicCollapsedList() for public users (users.js line 44-52)profile.cardCollapsed (line 267)setCardCollapsed() method (line 2088-2091)cardCollapsed() helper in cardDetails.js (line 100-107)Users.getPublicCardCollapsed() for public users (users.js line 80-85)profile.cardMaximized (line 260)toggleCardMaximized() mutation (line 1720-1726)hasCardMaximized() helper (line 1194-1196)profile.boardWorkspacesTree (line 1981-2026)setWorkspacesTree() method (line 1995-2000)profile.boardWorkspaceAssignments (line 2002-2011)assignBoardToWorkspace() and unassignBoardFromWorkspace() methodsprofile.boardView (line 1807)setBoardView() method (line 1805-1809)Storage Methods:
Cookies (via readCookieMap()/writeCookieMap()):
wekan-collapsed-lists - Collapsed list states (users.js line 44-58)wekan-collapsed-swimlanes - Collapsed swimlane stateslocalStorage:
wekan-list-widths - List widths (getListWidthFromStorage, line 1316-1327)wekan-swimlane-heights - Swimlane heights (setSwimlaneHeightToStorage, line 1100-1123)Coverage:
Severity: HIGH
Location: models/swimlanes.js lines 127, 251-263
Problem:
collapsed as a board-level field (defaults to false)isCollapsed() helper prioritizes per-user stored values from the user profileExpected Behavior: Per-user settings should be stored in profile.collapsedSwimlanes, not in the swimlane document itself.
Recommendation:
// CURRENT (WRONG):
collapsed: {
type: Boolean,
defaultValue: false, // Board-wide field
},
// SUGGESTED (CORRECT):
// Remove 'collapsed' from swimlane schema
// Only store per-user state in profile.collapsedSwimlanes
Severity: HIGH
Location: models/lists.js lines 147, 303-311
Problem:
collapsed fieldisCollapsed() helper checks per-user values firstcollapsed status between lists (fixMissingListsMigration.js line 165)Recommendation: Clarify whether collapsed state should be:
Severity: MEDIUM
Location: models/lists.js lines 48, 201-230, 275
Problem:
swimlaneId field but draggableLists() filters by boardId onlymyLists() which filters by both boardId and swimlaneIdQuestions:
Recommendation: Document the intended architecture clearly.
Severity: MEDIUM
Location: models/positionHistory.js
Problem:
Impact: Cannot determine full history of where a card/list was located over time
Recommendation: Consider extending to track all position changes with timestamps.
Severity: LOW
Location: models/users.js line 267, users.js line 2088-2091
Problem:
profile.cardCollapsed is a single boolean affecting all cards for a userRecommendation: Consider renaming to cardDetailsCollapsedByDefault or similar.
Severity: MEDIUM
Location: models/users.js lines 44-85
Problem:
Impact: Public/non-logged-in users lose UI preferences on page reload
Recommendation: Implement localStorage storage for all per-user preferences.
| Item | Status | Notes |
|---|---|---|
| Swimlane order persistence | ✅ | Via sort field, board-level |
| List order persistence | ✅ | Via sort field, board-level |
| Card order persistence | ✅ | Via sort field, card.move() |
| Checklist order persistence | ✅ | Via sort field |
| Checklist item order persistence | ✅ | Via sort field, ChecklistItems.move() |
| Swimlane color changes | ✅ | Via setColor() mutation |
| List color changes | ✅ | Via REST API or direct update |
| Card color changes | ✅ | Via setColor() mutation |
| Swimlane title changes | ✅ | Via rename() mutation, activity tracked |
| List title changes | ✅ | Via REST API or rename() mutation, activity tracked |
| Card title changes | ✅ | Via direct update, activity tracked |
| Checklist title changes | ✅ | Via setTitle() mutation |
| Checklist item title changes | ✅ | Via setTitle() mutation |
| Per-user list widths | ✅ | Via profile.listWidths |
| Per-user swimlane heights | ✅ | Via profile.swimlaneHeights |
| Per-user swimlane collapse state | ✅ | Via profile.collapsedSwimlanes |
| Per-user list collapse state | ✅ | Via profile.collapsedLists |
| Per-user card collapse state | ✅ | Via profile.cardCollapsed |
| Per-user board workspace organization | ✅ | Via profile.boardWorkspacesTree |
| Activity logging for changes | ✅ | Via Activities collection |
Clarify Collapsed State Architecture
Complete Public User Storage
Review Position History Usage
Audit Trail Feature
Data Integrity Tests
Database Indexes
sort fields for swimlanes, lists, cards, checklistsboardId fields for filteringDocument Persistence Model
Consistent Naming
cardCollapsed)| Entity | Field | Type | Persisted | Notes |
|---|---|---|---|---|
| Swimlane | title | Text | ✅ | Via rename() |
| Swimlane | sort | Number | ✅ | For ordering |
| Swimlane | color | String | ✅ | Via setColor() |
| Swimlane | collapsed | Boolean | ⚠️ | Issue #1: Conflicts with per-user storage |
| Swimlane | archived | Boolean | ✅ | Via archive()/restore() |
| List | title | Text | ✅ | Via rename() or REST |
| List | sort | Number | ✅ | For ordering |
| List | color | String | ✅ | Via REST or update |
| List | collapsed | Boolean | ⚠️ | Issue #2: Conflicts with per-user storage |
| List | starred | Boolean | ✅ | Via REST or update |
| List | wipLimit | Object | ✅ | Via REST or setWipLimit() |
| List | archived | Boolean | ✅ | Via archive() |
| Card | title | Text | ✅ | Direct update |
| Card | sort | Number | ✅ | Via move() |
| Card | color | String | ✅ | Via setColor() |
| Card | boardId/swimlaneId/listId | String | ✅ | Via move() |
| Card | archived | Boolean | ✅ | Via archive() |
| Card | description | Text | ✅ | Direct update |
| Card | customFields | Array | ✅ | Direct update |
| Checklist | title | Text | ✅ | Via setTitle() |
| Checklist | sort | Number | ✅ | Direct update |
| Checklist | hideCheckedChecklistItems | Boolean | ✅ | Via toggle mutation |
| Checklist | hideAllChecklistItems | Boolean | ✅ | Via toggle mutation |
| ChecklistItem | title | Text | ✅ | Via setTitle() |
| ChecklistItem | sort | Number | ✅ | Via move() |
| ChecklistItem | isFinished | Boolean | ✅ | Via check/uncheck/toggle |
| Setting | Storage | Scope | Notes |
|---|---|---|---|
| List Widths | profile.listWidths | Per-board, per-user | ✅ Working |
| Swimlane Heights | profile.swimlaneHeights | Per-board, per-user | ✅ Working |
| Collapsed Swimlanes | profile.collapsedSwimlanes | Per-board, per-user | ✅ Working |
| Collapsed Lists | profile.collapsedLists | Per-board, per-user | ✅ Working |
| Card Collapsed State | profile.cardCollapsed | Global per-user | ⚠️ Name misleading |
| Card Maximized State | profile.cardMaximized | Global per-user | ✅ Working |
| Board Workspaces | profile.boardWorkspacesTree | Global per-user | ✅ Working |
| Board Workspace Assignments | profile.boardWorkspaceAssignments | Global per-user | ✅ Working |
| Board View Style | profile.boardView | Global per-user | ✅ Working |