wekan/docs/Security/PerUserDataAudit2025-12-23/DATA_PERSISTENCE_ARCHITECTURE.md
Lauri Ojansivu a039bb1066
Some checks are pending
Docker / build (push) Waiting to run
Docker Image CI / build (push) Waiting to run
Release Charts / release (push) Waiting to run
Test suite / Meteor tests (push) Waiting to run
Test suite / Coverage report (push) Blocked by required conditions
Per-User and Board-level data save fixes. Part 3.
Thanks to xet7 !
2025-12-23 09:03:41 +02:00

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

  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

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