docs/Security/PerUserDataAudit2025-12-23/DATA_PERSISTENCE_ARCHITECTURE.md
Status: ✅ Latest Current
Updated: 2025-12-23
Scope: All data persistence related to swimlanes, lists, cards, checklists, checklistItems positioning and user preferences
Wekan's data persistence architecture distinguishes between:
This document defines the authoritative source of truth for all persistence decisions.
| Entity | Property | Storage | Format | Scope |
|---|---|---|---|---|
| Swimlane | Title | MongoDB | String | Board |
| Swimlane | Color | MongoDB | String (ALLOWED_COLORS) | Board |
| Swimlane | Background | MongoDB | Object {color} | Board |
| Swimlane | Height | MongoDB | Number (-1=auto, 50-2000) | Board |
| Swimlane | Position/Sort | MongoDB | Number (decimal) | Board |
| List | Title | MongoDB | String | Board |
| List | Color | MongoDB | String (ALLOWED_COLORS) | Board |
| List | Background | MongoDB | Object {color} | Board |
| List | Width | MongoDB | Number (100-1000) | Board |
| List | Position/Sort | MongoDB | Number (decimal) | Board |
| List | WIP Limit | MongoDB | Object {enabled, value, soft} | Board |
| List | Starred | MongoDB | Boolean | Board |
| Card | Title | MongoDB | String | Board |
| Card | Color | MongoDB | String (ALLOWED_COLORS) | Board |
| Card | Background | MongoDB | Object {color} | Board |
| Card | Description | MongoDB | String | Board |
| Card | Position/Sort | MongoDB | Number (decimal) | Board |
| Card | ListId | MongoDB | String | Board |
| Card | SwimlaneId | MongoDB | String | Board |
| Checklist | Title | MongoDB | String | Board |
| Checklist | Position/Sort | MongoDB | Number (decimal) | Board |
| Checklist | hideCheckedItems | MongoDB | Boolean | Board |
| Checklist | hideAllItems | MongoDB | Boolean | Board |
| ChecklistItem | Title | MongoDB | String | Board |
| ChecklistItem | isFinished | MongoDB | Boolean | Board |
| ChecklistItem | Position/Sort | MongoDB | Number (decimal) | Board |
| Entity | Property | Storage | Format | Users |
|---|---|---|---|---|
| User | Collapsed Swimlanes | User Profile / Cookie | Object {boardId: {swimlaneId: boolean}} | Single |
| User | Collapsed Lists | User Profile / Cookie | Object {boardId: {listId: boolean}} | Single |
| User | Hide Minicard Label Text | User Profile / localStorage | Object {boardId: boolean} | Single |
| User | Collapse Card Details View | Cookie | Boolean | Single |
Swimlanes.attachSchema(
new SimpleSchema({
title: { type: String }, // ✅ Per-board
color: { type: String, optional: true }, // ✅ Per-board (ALLOWED_COLORS)
// background: { ...color properties... } // ✅ Per-board (for future use)
height: { // ✅ Per-board (NEW)
type: Number,
optional: true,
defaultValue: -1, // -1 means auto-height
custom() {
const h = this.value;
if (h !== -1 && (h < 50 || h > 2000)) {
return 'heightOutOfRange';
}
},
},
sort: { type: Number, decimal: true, optional: true }, // ✅ Per-board
boardId: { type: String }, // ✅ Per-board
archived: { type: Boolean }, // ✅ Per-board
// NOTE: Collapse state is per-user only, stored in:
// - User profile: profile.collapsedSwimlanes[boardId][swimlaneId] = boolean
// - Non-logged-in: Cookie 'wekan-collapsed-swimlanes'
})
);
Lists.attachSchema(
new SimpleSchema({
title: { type: String }, // ✅ Per-board
color: { type: String, optional: true }, // ✅ Per-board (ALLOWED_COLORS)
// background: { ...color properties... } // ✅ Per-board (for future use)
width: { // ✅ Per-board (NEW)
type: Number,
optional: true,
defaultValue: 272, // default width in pixels
custom() {
const w = this.value;
if (w < 100 || w > 1000) {
return 'widthOutOfRange';
}
},
},
sort: { type: Number, decimal: true, optional: true }, // ✅ Per-board
swimlaneId: { type: String, optional: true }, // ✅ Per-board
boardId: { type: String }, // ✅ Per-board
archived: { type: Boolean }, // ✅ Per-board
wipLimit: { type: Object, optional: true }, // ✅ Per-board
starred: { type: Boolean, optional: true }, // ✅ Per-board
// NOTE: Collapse state is per-user only, stored in:
// - User profile: profile.collapsedLists[boardId][listId] = boolean
// - Non-logged-in: Cookie 'wekan-collapsed-lists'
})
);
Cards.attachSchema(
new SimpleSchema({
title: { type: String, optional: true }, // ✅ Per-board
color: { type: String, optional: true }, // ✅ Per-board (ALLOWED_COLORS)
// background: { ...color properties... } // ✅ Per-board (for future use)
description: { type: String, optional: true }, // ✅ Per-board
sort: { type: Number, decimal: true, optional: true }, // ✅ Per-board
swimlaneId: { type: String }, // ✅ Per-board (REQUIRED)
listId: { type: String, optional: true }, // ✅ Per-board
boardId: { type: String, optional: true }, // ✅ Per-board
archived: { type: Boolean }, // ✅ Per-board
// ... other fields are all per-board
})
);
Checklists.attachSchema(
new SimpleSchema({
title: { type: String }, // ✅ Per-board
sort: { type: Number, decimal: true }, // ✅ Per-board
hideCheckedChecklistItems: { type: Boolean, optional: true }, // ✅ Per-board
hideAllChecklistItems: { type: Boolean, optional: true }, // ✅ Per-board
cardId: { type: String }, // ✅ Per-board
})
);
ChecklistItems.attachSchema(
new SimpleSchema({
title: { type: String }, // ✅ Per-board
sort: { type: Number, decimal: true }, // ✅ Per-board
isFinished: { type: Boolean }, // ✅ Per-board
checklistId: { type: String }, // ✅ Per-board
cardId: { type: String }, // ✅ Per-board
})
);
// User.profile structure for per-user data
user.profile = {
// Collapse states - per-user, per-board
collapsedSwimlanes: {
'boardId123': {
'swimlaneId456': true, // swimlane is collapsed for this user
'swimlaneId789': false
},
'boardId999': { ... }
},
// Collapse states - per-user, per-board
collapsedLists: {
'boardId123': {
'listId456': true, // list is collapsed for this user
'listId789': false
},
'boardId999': { ... }
},
// Label visibility - per-user, per-board
hideMiniCardLabelText: {
'boardId123': true, // hide minicard labels on this board
'boardId999': false
}
}
For users not logged in, collapse state is persisted via cookies (localStorage alternative):
// Cookie: wekan-collapsed-swimlanes
{
'boardId123': {
'swimlaneId456': true,
'swimlaneId789': false
}
}
// Cookie: wekan-collapsed-lists
{
'boardId123': {
'listId456': true,
'listId789': false
}
}
// Cookie: wekan-card-collapsed
{
'state': false // is card details view collapsed
}
// localStorage: wekan-hide-minicard-label-{boardId}
true or false
1. User resizes swimlane in UI
2. Client calls: Swimlanes.update(swimlaneId, { $set: { height: 300 } })
3. MongoDB receives update
4. Schema validation: height must be -1 or 50-2000
5. Update stored in swimlanes collection: { _id, title, height: 300, ... }
6. Update reflected in Swimlanes collection reactive
7. All users viewing board see updated height
8. Persists across page reloads
9. Persists across browser restarts
1. User collapses swimlane in UI
2. Client detects LOGGED-IN or NOT-LOGGED-IN
3. If LOGGED-IN:
a. Client calls: Meteor.call('setCollapsedSwimlane', boardId, swimlaneId, true)
b. Server updates user profile: { profile: { collapsedSwimlanes: { ... } } }
c. Stored in users collection
4. If NOT-LOGGED-IN:
a. Client writes to cookie: wekan-collapsed-swimlanes
b. Stored in browser cookies
5. On next page load:
a. Client reads from profile (logged-in) or cookie (not logged-in)
b. UI restored to saved state
6. Collapse state NOT visible to other users
Add new fields to schemas
Swimlanes.height (default: -1)Lists.width (default: 272)Populate existing data
Remove per-user storage if present
Validation migration
| File | Purpose |
|---|---|
| models/swimlanes.js | Swimlane model with height field |
| models/lists.js | List model with width field |
| models/cards.js | Card model with position tracking |
| models/checklists.js | Checklist model |
| models/checklistItems.js | ChecklistItem model |
| models/users.js | User model with per-user settings |
| Term | Definition |
|---|---|
| Per-Board | Stored in swimlane/list/card document, visible to all users |
| Per-User | Stored in user profile/cookie, visible only to that user |
| Sort | Decimal number determining visual order of entity |
| Height | Pixel measurement of swimlane vertical size |
| Width | Pixel measurement of list horizontal size |
| Collapse | Hiding swimlane/list/card from view (per-user preference) |
| Position | Combination of swimlaneId/listId and sort value |
| Date | Change | Impact |
|---|---|---|
| 2025-12-23 | Created comprehensive architecture document | Documentation |
| 2025-12-23 | Added height field to Swimlanes | Per-board storage |
| 2025-12-23 | Added width field to Lists | Per-board storage |
| 2025-12-23 | Defined per-user data as collapse + label visibility | Architecture |
Status: ✅ Complete and Current
Next Review: Upon next architectural change