Back to Wekan

Wekan Data Persistence Architecture - 2025-12-23

docs/Security/PerUserDataAudit2025-12-23/DATA_PERSISTENCE_ARCHITECTURE.md

9.0813.8 KB
Original Source

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)

EntityPropertyStorageFormatScope
SwimlaneTitleMongoDBStringBoard
SwimlaneColorMongoDBString (ALLOWED_COLORS)Board
SwimlaneBackgroundMongoDBObject {color}Board
SwimlaneHeightMongoDBNumber (-1=auto, 50-2000)Board
SwimlanePosition/SortMongoDBNumber (decimal)Board
ListTitleMongoDBStringBoard
ListColorMongoDBString (ALLOWED_COLORS)Board
ListBackgroundMongoDBObject {color}Board
ListWidthMongoDBNumber (100-1000)Board
ListPosition/SortMongoDBNumber (decimal)Board
ListWIP LimitMongoDBObject {enabled, value, soft}Board
ListStarredMongoDBBooleanBoard
CardTitleMongoDBStringBoard
CardColorMongoDBString (ALLOWED_COLORS)Board
CardBackgroundMongoDBObject {color}Board
CardDescriptionMongoDBStringBoard
CardPosition/SortMongoDBNumber (decimal)Board
CardListIdMongoDBStringBoard
CardSwimlaneIdMongoDBStringBoard
ChecklistTitleMongoDBStringBoard
ChecklistPosition/SortMongoDBNumber (decimal)Board
ChecklisthideCheckedItemsMongoDBBooleanBoard
ChecklisthideAllItemsMongoDBBooleanBoard
ChecklistItemTitleMongoDBStringBoard
ChecklistItemisFinishedMongoDBBooleanBoard
ChecklistItemPosition/SortMongoDBNumber (decimal)Board

🔒 PER-USER ONLY (Private - User Profile or localStorage)

EntityPropertyStorageFormatUsers
UserCollapsed SwimlanesUser Profile / CookieObject {boardId: {swimlaneId: boolean}}Single
UserCollapsed ListsUser Profile / CookieObject {boardId: {listId: boolean}}Single
UserHide Minicard Label TextUser Profile / localStorageObject {boardId: boolean}Single
UserCollapse Card Details ViewCookieBooleanSingle

Implementation Details

1. Swimlanes Schema (swimlanes.js)

javascript
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)

javascript
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)

javascript
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)

javascript
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)

javascript
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)

javascript
// 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):

javascript
// 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

  1. Add new fields to schemas

    • Swimlanes.height (default: -1)
    • Lists.width (default: 272)
  2. Populate existing data

    • For swimlanes without height: set to -1 (auto)
    • For lists without width: set to 272 (default)
  3. 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
  4. 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

FilePurpose
models/swimlanes.jsSwimlane model with height field
models/lists.jsList model with width field
models/cards.jsCard model with position tracking
models/checklists.jsChecklist model
models/checklistItems.jsChecklistItem model
models/users.jsUser model with per-user settings

Glossary

TermDefinition
Per-BoardStored in swimlane/list/card document, visible to all users
Per-UserStored in user profile/cookie, visible only to that user
SortDecimal number determining visual order of entity
HeightPixel measurement of swimlane vertical size
WidthPixel measurement of list horizontal size
CollapseHiding swimlane/list/card from view (per-user preference)
PositionCombination of swimlaneId/listId and sort value

Change Log

DateChangeImpact
2025-12-23Created comprehensive architecture documentDocumentation
2025-12-23Added height field to SwimlanesPer-board storage
2025-12-23Added width field to ListsPer-board storage
2025-12-23Defined per-user data as collapse + label visibilityArchitecture

Status: ✅ Complete and Current
Next Review: Upon next architectural change