mirror of
https://github.com/wekan/wekan.git
synced 2025-12-31 14:48:48 +01:00
14 KiB
14 KiB
Wekan Data Persistence Architecture - 2025-12-23
Status: ✅ Latest Current
Updated: 2025-12-23
Scope: All data persistence related to swimlanes, lists, cards, checklists, checklistItems positioning and user preferences
Executive Summary
Wekan's data persistence architecture distinguishes between:
- Board-Level Data: Shared across all users on a board (positions, widths, heights, order)
- Per-User Data: Private to each user, not visible to others (collapse state, label visibility)
This document defines the authoritative source of truth for all persistence decisions.
Data Classification Matrix
✅ PER-BOARD LEVEL (Shared - Stored in MongoDB Documents)
| 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 |
🔒 PER-USER ONLY (Private - User Profile or localStorage)
| 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 |
Implementation Details
1. Swimlanes Schema (swimlanes.js)
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'
})
);
2. Lists Schema (lists.js)
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'
})
);
3. Cards Schema (cards.js)
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
})
);
4. Checklists Schema (checklists.js)
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
})
);
5. ChecklistItems Schema (checklistItems.js)
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
})
);
6. User Schema - Per-User Data (users.js)
// 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
}
}
Client-Side Storage (Non-Logged-In Users)
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
Data Flow
✅ Board-Level Data Flow (Swimlane Height Example)
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
✅ Per-User Data Flow (Collapse State Example)
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
Validation Rules
Swimlane Height Validation
- Allowed Values: -1 (auto) or 50-2000 pixels
- Default: -1 (auto)
- Trigger: On insert/update
- Action: Reject if invalid
List Width Validation
- Allowed Values: 100-1000 pixels
- Default: 272 pixels
- Trigger: On insert/update
- Action: Reject if invalid
Collapse State Validation
- Allowed Values: true or false
- Storage: Only boolean values allowed
- Trigger: On read/write to profile
- Action: Remove if corrupted
Migration Strategy
For Existing Installations
-
Add new fields to schemas
Swimlanes.height(default: -1)Lists.width(default: 272)
-
Populate existing data
- For swimlanes without height: set to -1 (auto)
- For lists without width: set to 272 (default)
-
Remove per-user storage if present
- Check user.profile.swimlaneHeights → migrate to swimlane.height
- Check user.profile.listWidths → migrate to list.width
- Remove old fields from user profile
-
Validation migration
- Ensure all swimlaneIds are valid (no orphaned data)
- Ensure all widths/heights are in valid range
- Clean corrupted per-user data
Security Implications
Per-User Data (🔒 Private)
- Collapse state is per-user → User A's collapse setting doesn't affect User B's view
- Hide label setting is per-user → User A's label visibility doesn't affect User B
- Stored in user profile → Only accessible to that user
- Cookies for non-logged-in → Stored locally, not transmitted
Per-Board Data (✅ Shared)
- Heights/widths are shared → All users see same swimlane/list sizes
- Positions are shared → All users see same card order
- Colors are shared → All users see same visual styling
- Stored in MongoDB → All users can query and receive updates
No Cross-User Leakage
- User A's preferences never stored in User B's profile
- User A's preferences never affect User B's view
- Each user has isolated per-user data space
Testing Checklist
Per-Board Data Tests
- Resize swimlane height → all users see change
- Resize list width → all users see change
- Move card between lists → all users see change
- Change card color → all users see change
- Reload page → changes persist
- Different browser → changes persist
Per-User Data Tests
- User A collapses swimlane → User B sees it expanded
- User A hides labels → User B sees labels
- User A scrolls away → User B can collapse same swimlane
- Logout → cookies maintain collapse state
- Login as different user → previous collapse state not visible
- Reload page → collapse state restored for user
Validation Tests
- Set swimlane height = 25 → rejected (< 50)
- Set swimlane height = 3000 → rejected (> 2000)
- Set list width = 50 → rejected (< 100)
- Set list width = 2000 → rejected (> 1000)
- Corrupt localStorage height → cleaned on startup
- Corrupt user profile height → cleaned on startup
Related Files
| 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 |
Glossary
| 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 |
Change Log
| 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