CatalyzeUpDocs
impact pulse / technical

Database Schema

MongoDB Database Schema

Overview

MongoDB schema design for ImpactPulse, optimized for flexibility, performance, and scalability.

Collections

users

{
  _id: ObjectId("..."),
  email: "user@example.com", // unique index
  password_hash: "bcrypt_hash",
  full_name: "John Doe",

  // Email & Auth
  email_verified: false,
  email_verification_token: "token",
  email_preferences: {
    reminders: true,
    updates: true,
    reports: false
  },

  // Activity
  created_at: ISODate("2024-01-15T10:00:00Z"),
  last_login_at: ISODate("2024-01-15T10:00:00Z"),

  // Organizations (denormalized for performance)
  organization_memberships: [
    ObjectId("org_id_1"),
    ObjectId("org_id_2")
  ],

  // Stats
  total_surveys_taken: 5,
  last_survey_at: ISODate("2024-01-15T10:00:00Z"),

  // Metadata
  timezone: "America/New_York",
  locale: "en_US",
  metadata: {}
}

organizations

{
  _id: ObjectId("..."),
  name: "Acme Corporation",
  slug: "acme-corp", // unique index
  description: "Leading innovation company",

  // Ownership
  created_by_id: ObjectId("user_id"),
  created_at: ISODate("2024-01-15T10:00:00Z"),

  // Subscription
  subscription_tier: "pro", // free, pro, enterprise
  subscription_expires_at: ISODate("2025-01-15T10:00:00Z"),

  // Settings
  settings: {
    allow_anonymous: false,
    max_surveys: 100,
    max_responses_per_survey: 1000,
    features: ["advanced_analytics", "api_access"],
    branding: {
      logo_url: "https://...",
      primary_color: "#0066CC"
    }
  },

  // Stats
  member_count: 25,
  survey_count: 10,
  total_responses: 250,

  // Metadata
  industry: "Technology",
  size: "50-100",
  country: "US",
  metadata: {}
}

organization_members

{
  _id: ObjectId("..."),
  organization_id: ObjectId("org_id"), // compound index with user_id
  user_id: ObjectId("user_id"),

  // Role & Permissions
  role: "admin", // owner, admin, member
  permissions: ["view_analytics", "manage_surveys"],

  // Activity
  joined_at: ISODate("2024-01-15T10:00:00Z"),
  invited_by_id: ObjectId("inviter_id"),
  last_active_at: ISODate("2024-01-15T10:00:00Z"),

  // Status
  is_active: true,
  deactivated_at: null,

  // Stats
  surveys_taken: 5,
  surveys_assigned: 8,
  completion_rate: 0.625
}

organization_invitations

{
  _id: ObjectId("..."),
  organization_id: ObjectId("org_id"),

  // Invitation Details
  email: "invitee@example.com", // index
  token: "secure_random_token", // unique index
  role: "member",

  // Tracking
  invited_by_id: ObjectId("admin_id"),
  created_at: ISODate("2024-01-15T10:00:00Z"),
  expires_at: ISODate("2024-01-22T10:00:00Z"), // 7 days

  // Status
  status: "pending", // pending, accepted, expired, cancelled
  accepted_at: null,
  accepted_by_user_id: null,

  // Custom Message
  custom_message: "Welcome to our team!",

  // Metadata
  invitation_source: "dashboard",
  metadata: {}
}

surveys

{
  _id: ObjectId("..."),
  title: "Q1 2024 Maslow Assessment",
  description: "Quarterly team wellbeing check",
  type: "maslow", // maslow, custom, feedback

  // Ownership
  created_by_id: ObjectId("user_id"),
  organization_id: ObjectId("org_id"), // index

  // Access Control
  public_id: "surv_abc123", // unique index for public access
  is_active: true,
  is_published: true,
  is_organization_only: true,

  // Multi-attempt Settings
  allow_multiple_attempts: true,
  max_attempts: null, // null = unlimited
  attempts_time_restriction: {
    enabled: true,
    minimum_days_between: 30
  },

  // Reminder Settings
  reminder_settings: {
    enabled: true,
    schedule: [24, 48, 168], // hours after start
    custom_message: "Don't forget to complete your assessment"
  },

  // Access Control
  access_control: {
    require_login: true,
    allowed_domains: ["acme.com"],
    invitation_only: false,
    assigned_users: [] // if invitation_only
  },

  // Timestamps
  created_at: ISODate("2024-01-15T10:00:00Z"),
  updated_at: ISODate("2024-01-15T10:00:00Z"),
  published_at: ISODate("2024-01-15T10:00:00Z"),

  // Stats
  total_responses: 150,
  completion_rate: 0.75,
  average_time_seconds: 420,

  // Scoring Summary (precomputed from question scoring_config values)
  scoring_summary: {
    max_possible_score: 40,       // sum of max_points for all scored questions
    scored_question_count: 4,     // number of questions with scoring (excludes qualitative)
    qualitative_question_count: 1 // number of qualitative (unscored) questions
  },

  // Metadata
  tags: ["quarterly", "wellbeing"],
  category: "hr",
  metadata: {}
}

questions

{
  _id: ObjectId("..."),
  survey_id: ObjectId("survey_id"), // index

  // Question Details
  text: "How satisfied are you with your work-life balance?",
  type: "rating", // rating, text, boolean, multiple_choice, checkbox

  // Display
  order: 1, // compound index with survey_id
  section: "Work Environment",

  // Validation
  required: true,
  validation: {
    min: 1,
    max: 10,
    pattern: null
  },

  // Options (for multiple choice/checkbox)
  options: [
    {
      value: "very_satisfied",
      label: "Very Satisfied",
      order: 1
    }
  ],

  // Maslow Categorization
  category: "safety", // physiological, safety, love_belonging, esteem, self_actualization
  weight: 1.0, // for scoring

  // Conditional Logic
  conditional_logic: {
    show_if: {
      question_id: ObjectId("..."),
      operator: "equals",
      value: "yes"
    }
  },

  // Scoring Configuration (see scoring-system.md for details)
  scoring_config: {
    scoring_type: "direct_scale", // direct_scale, binary, weighted_options, multi_select_sum, qualitative
    max_points: 10,               // maximum achievable points for this question (default: 10)
    scale_min: 0,                 // for direct_scale: minimum scale value (default: 0)
    scale_max: 5,                 // for direct_scale: maximum scale value (default: 5)
    positive_answer: "yes",       // for binary: which answer scores full points (default: "yes")
    option_weights: {             // for weighted_options and multi_select_sum (custom_weight mode)
      "Weekly": 10,
      "Monthly": 6,
      "Never": 0
    },
    mode: "equal_weight",         // for multi_select_sum: "equal_weight" or "custom_weight"
    weight_multiplier: 1.0        // optional multiplier for question importance (default: 1.0)
  },

  // Metadata
  help_text: "Consider your typical work week",
  metadata: {}
}

Scoring Configuration

The scoring_config field on each question defines how responses convert to numeric scores. There are five scoring types:

Scoring Type Used For Formula
direct_scale Rating questions (0-5) (rating / scale_max) * max_points
binary Yes/No questions positive_answer = max_points, else 0
weighted_options Single select / radio / dropdown Lookup from option_weights map
multi_select_sum Checkboxes / multi-select Sum of selected option weights, capped at max_points
qualitative Text / textarea Not scored (excluded from calculations)

For full scoring specification, formulas, and examples, see Scoring System.

survey_sessions

{
  _id: ObjectId("..."),
  user_id: ObjectId("user_id"), // compound index with survey_id, attempt_number
  survey_id: ObjectId("survey_id"),
  organization_id: ObjectId("org_id"), // index

  // Attempt Tracking
  attempt_number: 1, // incremental per user per survey

  // Status
  status: "in_progress", // not_started, in_progress, completed, abandoned

  // Progress
  current_question_index: 5,
  total_questions: 20,
  questions_answered: 5,
  required_questions_remaining: 10,

  // Partial Responses
  partial_responses: {
    "question_id_1": "answer",
    "question_id_2": 7,
    "question_id_3": ["option1", "option2"]
  },

  // Timestamps
  started_at: ISODate("2024-01-15T10:00:00Z"),
  last_activity_at: ISODate("2024-01-15T10:15:00Z"),
  completed_at: null,
  abandoned_at: null,

  // Time Tracking
  time_spent_seconds: 300,
  time_per_question: [30, 45, 20, 15, 50],

  // Context
  browser_info: {
    user_agent: "Mozilla/5.0...",
    browser: "Chrome",
    version: "120.0",
    os: "Windows 10",
    device_type: "desktop"
  },
  ip_address: "192.168.1.1",
  ip_country: "US",

  // Reminders
  reminders_sent: ["24h"],
  next_reminder: "48h",

  // Metadata
  referrer: "email",
  utm_params: {},
  metadata: {}
}

survey_responses

{
  _id: ObjectId("..."),
  session_id: ObjectId("session_id"), // index
  user_id: ObjectId("user_id"), // compound index with survey_id
  survey_id: ObjectId("survey_id"),
  organization_id: ObjectId("org_id"), // index

  // Attempt Info
  attempt_number: 1,

  // Full Responses
  responses: {
    "question_id_1": "Satisfied",
    "question_id_2": 8,
    "question_id_3": true,
    "question_id_4": ["option1", "option3"],
    "question_id_5": "Open text response..."
  },

  // Scoring (see scoring-system.md for full specification)
  scores: {
    total: 85,
    by_category: {
      "section_1": 90,
      "section_2": 80
    },
    percentile: 75 // compared to others
  },

  // Detailed Score Breakdown (points-based scoring system)
  score: {
    total_points: 34,            // sum of points earned across scored questions
    max_possible_points: 40,     // sum of max_points for all scored questions
    percentage: 85,              // (total_points / max_possible_points) * 100
    question_scores: [
      {
        question_id: ObjectId("q1"),
        points_earned: 8.0,
        max_points: 10,
        scoring_type: "direct_scale"
      },
      {
        question_id: ObjectId("q2"),
        points_earned: 10.0,
        max_points: 10,
        scoring_type: "binary"
      },
      {
        question_id: ObjectId("q3"),
        points_earned: 10.0,
        max_points: 10,
        scoring_type: "weighted_options"
      },
      {
        question_id: ObjectId("q4"),
        points_earned: 6.0,
        max_points: 10,
        scoring_type: "multi_select_sum"
      }
      // qualitative questions are excluded from scoring
    ]
  },

  // Timestamps
  submitted_at: ISODate("2024-01-15T10:30:00Z"),
  completed_at: ISODate("2024-01-15T10:30:00Z"),

  // Time Tracking
  time_spent_seconds: 600,

  // Validation
  is_complete: true,
  is_valid: true,
  validation_errors: [],

  // Metadata
  submission_source: "web",
  metadata: {}
}

maslow_scores

{
  _id: ObjectId("..."),
  user_id: ObjectId("user_id"), // index
  session_id: ObjectId("session_id"),
  response_id: ObjectId("response_id"),
  survey_id: ObjectId("survey_id"), // index

  // Attempt Info
  attempt_number: 2,

  // Scores by Category
  scores: {
    physiological: 0.85,      // 0-1 scale
    safety: 0.72,
    love_belonging: 0.68,
    esteem: 0.61,
    self_actualization: 0.55,
    overall: 0.682            // weighted average
  },

  // Raw Scores (before normalization)
  raw_scores: {
    physiological: 42,         // out of 50
    safety: 36,               // out of 50
    love_belonging: 34,       // out of 50
    esteem: 30.5,            // out of 50
    self_actualization: 27.5  // out of 50
  },

  // Comparison with Previous
  comparison_with_previous: {
    attempt_compared: 1,
    physiological_delta: 0.05,
    safety_delta: -0.03,
    love_belonging_delta: 0.08,
    esteem_delta: 0.11,
    self_actualization_delta: 0.15,
    overall_delta: 0.072,
    days_between: 92
  },

  // Percentiles (compared to population)
  percentiles: {
    physiological: 75,
    safety: 60,
    love_belonging: 55,
    esteem: 45,
    self_actualization: 40,
    overall: 55
  },

  // Insights
  insights: {
    strongest_area: "physiological",
    weakest_area: "self_actualization",
    biggest_improvement: "self_actualization",
    biggest_decline: "safety",
    consistency_score: 0.82
  },

  // Timestamps
  calculated_at: ISODate("2024-01-15T10:30:00Z"),

  // Metadata
  calculation_version: "1.0",
  metadata: {}
}

email_reminders

{
  _id: ObjectId("..."),
  user_id: ObjectId("user_id"), // index
  session_id: ObjectId("session_id"),
  survey_id: ObjectId("survey_id"),

  // Reminder Details
  reminder_type: "24h", // 24h, 48h, 7d, custom
  reminder_number: 1, // 1st, 2nd, 3rd reminder

  // Scheduling
  scheduled_at: ISODate("2024-01-16T10:00:00Z"), // compound index with status
  sent_at: null,

  // Email Details
  email_to: "user@example.com",
  email_subject: "Complete your Maslow Assessment",
  email_template: "survey_reminder_24h",

  // Tracking
  email_provider_id: "sendgrid_message_id",
  opened_at: null,
  clicked_at: null,
  unsubscribed_at: null,

  // Status
  status: "pending", // pending, sent, failed, cancelled, bounced

  // Retry Logic
  retry_count: 0,
  max_retries: 3,
  last_retry_at: null,
  error_message: null,

  // Custom Data
  custom_message: null,
  resume_link: "https://app.com/survey/abc/resume",

  // Metadata
  campaign_id: "q1_2024_maslow",
  metadata: {}
}

Indexes

Performance Indexes

// Users
db.users.createIndex({ "email": 1 }, { unique: true })
db.users.createIndex({ "created_at": -1 })
db.users.createIndex({ "organization_memberships": 1 })

// Organizations
db.organizations.createIndex({ "slug": 1 }, { unique: true })
db.organizations.createIndex({ "created_by_id": 1 })

// Organization Members
db.organization_members.createIndex(
  { "organization_id": 1, "user_id": 1 }, { unique: true }
)
db.organization_members.createIndex({ "user_id": 1 })
db.organization_members.createIndex({ "role": 1 })

// Surveys
db.surveys.createIndex({ "organization_id": 1 })
db.surveys.createIndex({ "public_id": 1 }, { unique: true })
db.surveys.createIndex({ "created_by_id": 1 })
db.surveys.createIndex({ "is_active": 1, "is_published": 1 })

// Questions
db.questions.createIndex({ "survey_id": 1, "order": 1 })
db.questions.createIndex({ "survey_id": 1 })

// Survey Sessions
db.survey_sessions.createIndex(
  { "user_id": 1, "survey_id": 1, "attempt_number": 1 }, { unique: true }
)
db.survey_sessions.createIndex({ "status": 1 })
db.survey_sessions.createIndex({ "organization_id": 1 })
db.survey_sessions.createIndex({ "last_activity_at": 1 })

// Survey Responses
db.survey_responses.createIndex({ "user_id": 1, "survey_id": 1 })
db.survey_responses.createIndex({ "organization_id": 1 })
db.survey_responses.createIndex({ "submitted_at": -1 })
db.survey_responses.createIndex({ "session_id": 1 })

// Maslow Scores
db.maslow_scores.createIndex({ "user_id": 1, "attempt_number": -1 })
db.maslow_scores.createIndex({ "survey_id": 1 })
db.maslow_scores.createIndex({ "calculated_at": -1 })

// Email Reminders
db.email_reminders.createIndex({ "scheduled_at": 1, "status": 1 })
db.email_reminders.createIndex({ "user_id": 1 })
db.email_reminders.createIndex({ "session_id": 1 })

TTL Indexes

// Auto-delete expired invitations
db.organization_invitations.createIndex(
  { "expires_at": 1 },
  { expireAfterSeconds: 0 }
)

// Clean up abandoned sessions after 90 days
db.survey_sessions.createIndex(
  { "last_activity_at": 1 },
  {
    expireAfterSeconds: 7776000, // 90 days
    partialFilterExpression: { status: "abandoned" }
  }
)

Query Patterns

Common Queries

// Get user's organizations
db.organization_members.find({ user_id: ObjectId("...") })

// Get organization's active surveys
db.surveys.find({
  organization_id: ObjectId("..."),
  is_active: true,
  is_published: true
})

// Get user's survey attempts
db.survey_sessions.find({
  user_id: ObjectId("..."),
  survey_id: ObjectId("...")
}).sort({ attempt_number: -1 })

// Get pending reminders
db.email_reminders.find({
  scheduled_at: { $lte: new Date() },
  status: "pending"
})

// Calculate organization stats
db.survey_responses.aggregate([
  { $match: { organization_id: ObjectId("...") } },
  { $group: {
    _id: "$survey_id",
    total_responses: { $sum: 1 },
    avg_score: { $avg: "$scores.total" }
  }}
])

Performance Considerations

Denormalization

  • User's organization memberships in user document
  • Survey stats cached in survey document
  • Member count cached in organization document

Aggregation Pipelines

// Maslow score trends
db.maslow_scores.aggregate([
  { $match: { user_id: ObjectId("...") } },
  { $sort: { attempt_number: 1 } },
  { $project: {
    attempt: "$attempt_number",
    date: "$calculated_at",
    scores: "$scores"
  }}
])