     1→/**
     2→ * Weekly 0.1% Excellence Report Job
     3→ * Runs every Monday at 7:15am AEST
     4→ * Analyzes usage, detects friction, suggests improvements
     5→ */
     6→
     7→const { runWeeklyAnalysis, getLatestReport } = require('../services/weeklyIntelligence');
     8→const { sendSlackMessage } = require('../services/slack');
     9→// Use supabaseAdmin to bypass RLS - jobs run without user context
    10→const { supabaseAdmin, supabase } = require('../config/database');
    11→const db = supabaseAdmin || supabase;
    12→
    13→/**
    14→ * Format the weekly report for Slack
    15→ */
    16→function formatSlackReport(report) {
    17→  const {
    18→    week_start,
    19→    week_end,
    20→    friction_points = [],
    21→    quick_wins = [],
    22→    patterns_detected = [],
    23→    unused_features = [],
    24→    ai_quality = {},
    25→    overall_health_score = 0,
    26→    friction_score = 0,
    27→    improvement_opportunities = 0
    28→  } = report;
    29→
    30→  // Health score emoji
    31→  const healthEmoji = overall_health_score >= 80 ? ':white_check_mark:' :
    32→                      overall_health_score >= 60 ? ':warning:' : ':x:';
    33→
    34→  // Build the message
    35→  let message = `*:chart_with_upwards_trend: WEEKLY 0.1% EXCELLENCE REVIEW*\n`;
    36→  message += `Week of ${formatDate(week_start)} - ${formatDate(week_end)}\n\n`;
    37→
    38→  // Health Score
    39→  message += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
    40→  message += `${healthEmoji} *System Health: ${overall_health_score}/100*\n`;
    41→  message += `Friction Score: ${friction_score}/100 | Opportunities: ${improvement_opportunities}\n\n`;
    42→
    43→  // Top Friction Point
    44→  if (friction_points.length > 0) {
    45→    const topFriction = friction_points[0];
    46→    message += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
    47→    message += `:warning: *TOP FRICTION POINT*\n`;
    48→    message += `*${topFriction.location || 'Unknown location'}*\n`;
    49→    message += `${topFriction.issue || 'Issue detected'}\n`;
    50→    if (topFriction.evidence) {
    51→      message += `Evidence: ${typeof topFriction.evidence === 'string' ? topFriction.evidence : JSON.stringify(topFriction.evidence)}\n`;
    52→    }
    53→    message += `→ FIX: ${topFriction.suggested_fix || 'Review needed'}\n`;
    54→    message += `→ EFFORT: ${(topFriction.effort || 'medium').toUpperCase()}\n\n`;
    55→  }
    56→
    57→  // Quick Win
    58→  if (quick_wins.length > 0) {
    59→    const topWin = quick_wins[0];
    60→    message += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
    61→    message += `:zap: *QUICK WIN OPPORTUNITY*\n`;
    62→    message += `*${topWin.title || 'Improvement opportunity'}*\n`;
    63→    message += `${topWin.description || ''}\n`;
    64→    message += `→ IMPACT: ${topWin.impact || 'Positive impact expected'}\n`;
    65→    message += `→ EFFORT: ${(topWin.effort || 'low').toUpperCase()}\n\n`;
    66→  }
    67→
    68→  // Pattern Detected
    69→  if (patterns_detected.length > 0) {
    70→    const pattern = patterns_detected[0];
    71→    message += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
    72→    message += `:bulb: *PATTERN DETECTED*\n`;
    73→    message += `${pattern.pattern || 'Pattern identified'}\n`;
    74→    if (pattern.actionable_insight) {
    75→      message += `→ ${pattern.actionable_insight}\n`;
    76→    }
    77→    message += `\n`;
    78→  }
    79→
    80→  // Unused Feature Alert
    81→  if (unused_features.length > 0) {
    82→    const unused = unused_features[0];
    83→    message += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
    84→    message += `:zzz: *UNUSED FEATURE ALERT*\n`;
    85→    message += `*${unused.feature || 'Feature'}*: ${unused.current_usage || 'Low usage'}\n`;
    86→    message += `→ Recommendation: ${(unused.recommendation || 'investigate').toUpperCase()}\n\n`;
    87→  }
    88→
    89→  // AI Quality
    90→  if (ai_quality.total > 0) {
    91→    message += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
    92→    message += `:robot_face: *AI QUALITY CHECK*\n`;
    93→    const acceptEmoji = (ai_quality.acceptance_rate || 0) >= 80 ? ':white_check_mark:' :
    94→                        (ai_quality.acceptance_rate || 0) >= 60 ? ':warning:' : ':x:';
    95→    message += `${acceptEmoji} Overall acceptance: ${ai_quality.acceptance_rate || 0}% (${ai_quality.positive}/${ai_quality.total})\n\n`;
    96→  }
    97→
    98→  // Summary
    99→  message += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
   100→  message += `:clipboard: *PRIORITY RECOMMENDATIONS*\n`;
   101→  if (quick_wins.length > 0) {
   102→    message += `1. [LOW EFFORT] ${quick_wins[0].title || 'Quick win'}\n`;
   103→  }
   104→  if (friction_points.length > 0) {
   105→    message += `2. [HIGH IMPACT] Fix ${friction_points[0].location || 'friction point'}\n`;
   106→  }
   107→  if (unused_features.length > 0) {
   108→    message += `3. [INVESTIGATE] ${unused_features[0].feature || 'Feature'} usage decline\n`;
   109→  }
   110→
   111→  return {
   112→    text: message,
   113→    unfurl_links: false,
   114→    unfurl_media: false
   115→  };
   116→}
   117→
   118→/**
   119→ * Format date for display
   120→ */
   121→function formatDate(dateStr) {
   122→  const date = new Date(dateStr);
   123→  return date.toLocaleDateString('en-AU', { month: 'short', day: 'numeric' });
   124→}
   125→
   126→/**
   127→ * Run the weekly excellence report job
   128→ */
   129→async function runWeeklyExcellenceReport() {
   130→  console.log('\n:chart_with_upwards_trend: Starting Weekly 0.1% Excellence Report...\n');
   131→
   132→  try {
   133→    // Run the analysis
   134→    const report = await runWeeklyAnalysis();
   135→
   136→    // Format for Slack
   137→    const slackPayload = formatSlackReport(report);
   138→
   139→    // Send to Slack
   140→    const slackResult = await sendSlackMessage(slackPayload);
   141→
   142→    // Store the Slack message timestamp for threading
   143→    if (slackResult?.ts) {
   144→      await db
   145→        .from('weekly_excellence_reports')
   146→        .update({
   147→          slack_message_ts: slackResult.ts,
   148→          slack_channel_id: slackResult.channel
   149→        })
   150→        .eq('id', report.id);
   151→
   152→      console.log('Slack thread saved for follow-up messages');
   153→    }
   154→
   155→    console.log(':white_check_mark: Weekly excellence report sent to Slack');
   156→    return report;
   157→  } catch (error) {
   158→    console.error(':x: Weekly excellence report failed:', error);
   159→
   160→    // Send error notification
   161→    await sendSlackMessage({
   162→      text: `:x: Weekly 0.1% Excellence Report failed to generate: ${error.message}`
   163→    });
   164→
   165→    throw error;
   166→  }
   167→}
   168→
   169→/**
   170→ * Send a follow-up message in the weekly report thread
   171→ */
   172→async function sendThreadedUpdate(message) {
   173→  try {
   174→    // Get the latest report with Slack thread info
   175→    const report = await getLatestReport();
   176→
   177→    if (!report?.slack_message_ts || !report?.slack_channel_id) {
   178→      console.log('No Slack thread found for this week, sending as new message');
   179→      return sendSlackMessage({ text: message });
   180→    }
   181→
   182→    // Send as thread reply
   183→    return sendSlackMessage({
   184→      text: message,
   185→      channel: report.slack_channel_id,
   186→      thread_ts: report.slack_message_ts
   187→    });
   188→  } catch (error) {
   189→    console.error('Failed to send threaded update:', error);
   190→    // Fallback to regular message
   191→    return sendSlackMessage({ text: message });
   192→  }
   193→}
   194→
   195→/**
   196→ * Manually trigger the report (for testing)
   197→ */
   198→async function triggerManually() {
   199→  return runWeeklyExcellenceReport();
   200→}
   201→
   202→module.exports = {
   203→  runWeeklyExcellenceReport,
   204→  sendThreadedUpdate,
   205→  triggerManually,
   206→  formatSlackReport
   207→};
   208→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→/**
     2→ * Platform Sync Job
     3→ * Polls RateRight API to sync user activity
     4→ */
     5→
     6→// Use supabaseAdmin to bypass RLS - jobs run without user context
     7→const { supabaseAdmin, supabase } = require('../config/database');
     8→const db = supabaseAdmin || supabase;
     9→
    10→const RATERIGHT_API_URL = process.env.RATERIGHT_API_URL || 'https://rateright.com.au/api';
    11→const RATERIGHT_API_KEY = process.env.RATERIGHT_ADMIN_API_KEY;
    12→
    13→/**
    14→ * Fetch recent activity from RateRight platform
    15→ * @returns {Promise<Array>}
    16→ */
    17→async function fetchPlatformActivity() {
    18→  if (!RATERIGHT_API_KEY) {
    19→    console.warn('Platform sync: RATERIGHT_ADMIN_API_KEY not configured');
    20→    return [];
    21→  }
    22→
    23→  try {
    24→    const response = await fetch(`${RATERIGHT_API_URL}/admin/activity`, {
    25→      headers: {
    26→        'Authorization': `Bearer ${RATERIGHT_API_KEY}`,
    27→        'Content-Type': 'application/json'
    28→      }
    29→    });
    30→
    31→    if (!response.ok) {
    32→      throw new Error(`API error: ${response.status}`);
    33→    }
    34→
    35→    const data = await response.json();
    36→    return data.activities || [];
    37→  } catch (error) {
    38→    console.error('Failed to fetch platform activity:', error.message);
    39→    return [];
    40→  }
    41→}
    42→
    43→/**
    44→ * Sync platform activity to local database
    45→ * @returns {Promise<{synced: number, errors: number}>}
    46→ */
    47→async function syncPlatformActivity() {
    48→  if (!db) {
    49→    return { synced: 0, errors: 0, message: 'Database not configured' };
    50→  }
    51→
    52→  const activities = await fetchPlatformActivity();
    53→
    54→  if (activities.length === 0) {
    55→    return { synced: 0, errors: 0, message: 'No activities to sync' };
    56→  }
    57→
    58→  let synced = 0;
    59→  let errors = 0;
    60→
    61→  for (const activity of activities) {
    62→    try {
    63→      // Find matching lead by email or phone
    64→      let lead = null;
    65→
    66→      if (activity.email) {
    67→        const { data } = await db
    68→          .from('leads')
    69→          .select('id')
    70→          .eq('email', activity.email)
    71→          .single();
    72→        lead = data;
    73→      }
    74→
    75→      if (!lead && activity.phone) {
    76→        const { data } = await db
    77→          .from('leads')
    78→          .select('id')
    79→          .eq('phone', activity.phone)
    80→          .single();
    81→        lead = data;
    82→      }
    83→
    84→      if (!lead) {
    85→        // Create new lead from platform activity
    86→        const { data: newLead, error: createError } = await db
    87→          .from('leads')
    88→          .insert({
    89→            first_name: activity.first_name,
    90→            last_name: activity.last_name,
    91→            email: activity.email,
    92→            phone: activity.phone,
    93→            company: activity.company,
    94→            source: 'platform',
    95→            status: 'new',
    96→            score: 0
    97→          })
    98→          .select('id')
    99→          .single();
   100→
   101→        if (createError) {
   102→          errors++;
   103→          continue;
   104→        }
   105→        lead = newLead;
   106→      }
   107→
   108→      // Log platform activity
   109→      const { error: activityError } = await db
   110→        .from('platform_activity')
   111→        .insert({
   112→          lead_id: lead.id,
   113→          activity_type: activity.type,
   114→          description: activity.description,
   115→          activity_at: activity.timestamp || new Date().toISOString(),
   116→          metadata: activity.metadata || {}
   117→        });
   118→
   119→      if (activityError) {
   120→        errors++;
   121→      } else {
   122→        synced++;
   123→
   124→        // Update lead score for significant activities
   125→        if (['job_posted', 'worker_hired', 'login'].includes(activity.type)) {
   126→          const { updateLeadScore } = require('../services/scoring');
   127→          await updateLeadScore(lead.id);
   128→        }
   129→      }
   130→    } catch (error) {
   131→      console.error('Activity sync error:', error.message);
   132→      errors++;
   133→    }
   134→  }
   135→
   136→  console.log(`Platform sync: ${synced} synced, ${errors} errors`);
   137→  return { synced, errors };
   138→}
   139→
   140→/**
   141→ * Run platform sync job
   142→ */
   143→async function runPlatformSync() {
   144→  console.log('Starting platform sync...');
   145→  const startTime = Date.now();
   146→
   147→  const result = await syncPlatformActivity();
   148→
   149→  const duration = Date.now() - startTime;
   150→  console.log(`Platform sync completed in ${duration}ms`);
   151→
   152→  return result;
   153→}
   154→
   155→module.exports = {
   156→  fetchPlatformActivity,
   157→  syncPlatformActivity,
   158→  runPlatformSync
   159→};
   160→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→/**
     2→ * Batch Refresh Call List Scores
     3→ *
     4→ * Runs periodically to update stale call_list_reason and call_list_score
     5→ * fields in the leads table. Uses the same ranking logic as the live API
     6→ * but batches the context build for efficiency.
     7→ */
     8→
     9→const { supabase } = require('../config/database');
    10→const { computeLeadPriority, buildRankingContext } = require('../services/callListRanker');
    11→
    12→const JOB_NAME = 'refreshCallListScores';
    13→
    14→async function run() {
    15→  if (!supabase) {
    16→    console.log(`[${JOB_NAME}] Database not configured, skipping`);
    17→    return { success: false, reason: 'no_database' };
    18→  }
    19→
    20→  const startTime = Date.now();
    21→  console.log(`[${JOB_NAME}] Starting batch refresh...`);
    22→
    23→  try {
    24→    // Build ranking context ONCE (this is the expensive part)
    25→    const context = await buildRankingContext();
    26→
    27→    // Fetch all pending callbacks in one query
    28→    const { data: allCallbacks } = await supabase
    29→      .from('callbacks')
    30→      .select('*')
    31→      .eq('status', 'pending');
    32→
    33→    // Build callback map for O(1) lookup
    34→    const callbackMap = new Map();
    35→    for (const cb of (allCallbacks || [])) {
    36→      if (!callbackMap.has(cb.lead_id)) {
    37→        callbackMap.set(cb.lead_id, []);
    38→      }
    39→      callbackMap.get(cb.lead_id).push(cb);
    40→    }
    41→
    42→    // Find leads with stale call_list scores (not refreshed in 4 hours)
    43→    const fourHoursAgo = new Date(Date.now() - 4 * 60 * 60 * 1000).toISOString();
    44→
    45→    const { data: staleLeads, error } = await supabase
    46→      .from('leads')
    47→      .select('*')
    48→      .is('deleted_at', null)
    49→      .not('status', 'in', '("converted","opted_out","archived")')
    50→      .or(`call_list_updated_at.is.null,call_list_updated_at.lt.${fourHoursAgo}`)
    51→      .order('score', { ascending: false })
    52→      .limit(200);
    53→
    54→    if (error) {
    55→      console.error(`[${JOB_NAME}] Failed to fetch stale leads:`, error);
    56→      return { success: false, error: error.message };
    57→    }
    58→
    59→    if (!staleLeads || staleLeads.length === 0) {
    60→      console.log(`[${JOB_NAME}] No stale leads found`);
    61→      return { success: true, refreshed: 0 };
    62→    }
    63→
    64→    console.log(`[${JOB_NAME}] Refreshing ${staleLeads.length} leads...`);
    65→
    66→    let refreshed = 0;
    67→    let errors = 0;
    68→
    69→    // Process in batches of 20 for efficient DB updates
    70→    const batchSize = 20;
    71→    for (let i = 0; i < staleLeads.length; i += batchSize) {
    72→      const batch = staleLeads.slice(i, i + batchSize);
    73→
    74→      const updates = [];
    75→      for (const lead of batch) {
    76→        try {
    77→          const leadCallbacks = callbackMap.get(lead.id) || [];
    78→          const priority = await computeLeadPriority(lead, context, leadCallbacks);
    79→          updates.push({
    80→            id: lead.id,
    81→            call_list_score: priority.priority_score,
    82→            call_list_reason: priority.priority_reason,
    83→            call_list_reason_type: priority.priority_type,
    84→            call_list_warning: priority.priority_warning,
    85→            call_list_updated_at: new Date().toISOString()
    86→          });
    87→        } catch (err) {
    88→          errors++;
    89→          console.warn(`[${JOB_NAME}] Error computing priority for ${lead.id}: ${err.message}`);
    90→        }
    91→      }
    92→
    93→      // Batch update via individual updates (Supabase doesn't support bulk upsert on arbitrary columns easily)
    94→      for (const update of updates) {
    95→        const { error: updateError } = await supabase
    96→          .from('leads')
    97→          .update({
    98→            call_list_score: update.call_list_score,
    99→            call_list_reason: update.call_list_reason,
   100→            call_list_reason_type: update.call_list_reason_type,
   101→            call_list_warning: update.call_list_warning,
   102→            call_list_updated_at: update.call_list_updated_at
   103→          })
   104→          .eq('id', update.id);
   105→
   106→        if (updateError) {
   107→          errors++;
   108→          console.warn(`[${JOB_NAME}] Failed to update ${update.id}: ${updateError.message}`);
   109→        } else {
   110→          refreshed++;
   111→        }
   112→      }
   113→    }
   114→
   115→    const duration = Date.now() - startTime;
   116→    console.log(`[${JOB_NAME}] Done: ${refreshed} refreshed, ${errors} errors in ${duration}ms`);
   117→
   118→    return { success: true, refreshed, errors, duration };
   119→
   120→  } catch (error) {
   121→    console.error(`[${JOB_NAME}] Unexpected error:`, error);
   122→    return { success: false, error: error.message };
   123→  }
   124→}
   125→
   126→module.exports = { run, JOB_NAME };
   127→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


Now let me check for the .env file and check what environment variables are used.


TodoWrite

Read entry point and config files

Read all middleware files

Read all utility files

Read all job files (batch 1)

Read all job files (batch 2)

Read .env and remaining files

Compile comprehensive audit report


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable


Bash

ls -la /home/ccuser/rateright-growth/.env 2>/dev/null; echo "---"; if [ -f /home/ccuser/rateright-growth/.env ]; then awk -F= '{print $1}' /home/ccuser/rateright-growth/.env | head -80; fi

Check if .env exists and list variable names only


-rw------- 1 ccuser ccuser 1812 Feb  7 07:32 /home/ccuser/rateright-growth/.env
---
# Supabase
SUPABASE_URL
SUPABASE_ANON_KEY
SUPABASE_SERVICE_ROLE_KEY

# Notion Integration
NOTION_API_KEY
NOTION_BUSINESS_STATE_DB
NOTION_TASKS_DB
NOTION_KNOWLEDGE_DB
NOTION_TEAM_DB
NOTION_METRICS_DB

# SMS Control (Sales/Marketing Paused)
DISABLE_ALL_SMS
SMS_PAUSE_REASON

# Twilio (for SMS and Voice)
TWILIO_ACCOUNT_SID
TWILIO_AUTH_TOKEN
TWILIO_PHONE_NUMBER
TWILIO_API_KEY_SID
TWILIO_API_KEY_SECRET
TWILIO_TWIML_APP_SID

# Vapi (for AI voice assistant)
VAPI_API_KEY
VAPI_PUBLIC_KEY
VAPI_ASSISTANT_ID
VAPI_PHONE_NUMBER_ID

# OpenAI (for AI features)
OPENAI_API_KEY

# Server Configuration
APP_URL
INTERNAL_JOB_KEY

# Clawdbot API Access
CLAWDBOT_API_KEY

# Apollo (for company enrichment)
APOLLO_API_KEY


Now let me compile the comprehensive audit report.


TodoWrite

Read entry point and config files

Read all middleware files

Read all utility files

Read all job files (batch 1)

Read all job files (batch 2)

Read .env and remaining files

Compile comprehensive audit report


Read entry point and config files

Read all middleware files

Read all utility files

Read all job files (batch 1)

Read all job files (batch 2)

Read .env and remaining files

Compile comprehensive audit report


---

# Comprehensive Codebase Audit Report

## SECTION 1: ENTRY POINT AND ROUTE MOUNTING

**File:** `/home/ccuser/rateright-growth/src/index.js` (460 lines)

### Purpose
Express.js application entry point. Creates an HTTP server with Express, mounts ~40 API route groups, sets up two WebSocket servers (Deepgram transcription proxy and Twilio media streams), configures middleware, and starts scheduled jobs on database connection success.

### How Routes Are Mounted

**Public (no auth required):**
- `/health` and `/api/health` -- health check
- `/api/webhooks` -- Twilio SMS webhooks (webhookLimiter only)
- `/api/voice` -- Twilio voice webhooks (webhookLimiter + Twilio signature validation)
- `/api/vapi` -- Vapi AI voice webhooks (webhookLimiter + Vapi signature validation)
- `/api/jobs/internal` -- internal job triggers (writeLimiter, X-Internal-Key header)
- `/api/leads/internal` -- internal leads endpoint for Clawdbot (writeLimiter, X-Internal-Key header)
- `/api/intelligence` -- usage tracking (trackingLimiter, NO auth) **(line 258)**
- `/api/frontend` -- web vitals/error tracking (trackingLimiter, NO auth) **(line 259)**
- `/api/notifications/vapid-public-key` -- VAPID key endpoint (no auth)

**Protected (requireAuth or requireApiKeyOrAuth):**
All remaining routes use `requireAuth` or `requireApiKeyOrAuth(requireAuth)` with various rate limiters.

### Issues Found

1. **SECURITY: Unauthenticated tracking endpoints** (lines 258-259): `/api/intelligence` and `/api/frontend` have no auth -- only IP-based rate limiting (60/min). An attacker could flood the `usage_events` table with garbage data since there is no user validation.

2. **SECURITY: Hardcoded notification routes in entry point** (lines 126-212): Push notification subscribe, status, preferences, and test endpoints are embedded directly in `index.js` instead of a route file. The preferences PUT endpoint (line 194-207) accepts arbitrary body fields and writes them to the database after only stripping `id` and `user_id` -- no schema validation.

3. **SECURITY: WebSocket `/api/transcribe/ws` has no authentication** (lines 304-431): The Deepgram transcription proxy WebSocket accepts connections without any token validation. Anyone who can reach the server can connect and stream audio, consuming Deepgram API credits. The API key is also logged with its length (line 334).

4. **SECURITY: WebSocket `/api/twilio-media` has no authentication** (lines 433-438): While Twilio would normally be the only client, there is no origin/signature check.

5. **Content Security Policy disabled** (line 94): `contentSecurityPolicy: false` in Helmet config disables XSS protection headers for the entire app, not just admin UI routes.

6. **Duplicate route mounting**: `/api/battleground` is mounted twice (lines 250-251) -- once for V1 routes and once for V2 routes. This could cause route conflicts if both define handlers for the same paths.

7. **Duplicate route mounting**: `/api/webhooks` is mounted twice -- once at line 123 (no auth, for inbound Twilio) and again at line 273 (requireAuth, for outbound webhooks). Express will try the first match, potentially shadowing the authenticated routes.

---

## SECTION 2: PACKAGE.JSON

**File:** `/home/ccuser/rateright-growth/package.json`

### Key Dependencies
- Express 4.18.2, Supabase JS 2.39.0, Twilio 4.19.0
- OpenAI 6.16.0, Deepgram (via WebSocket, not a direct dep)
- Redis: Both `ioredis` 5.9.2 AND `redis` 5.10.0 are installed (potential redundancy)
- Security: helmet, cors, hpp (HTTP parameter pollution), express-mongo-sanitize, xss
- `web-push` 3.6.7 for push notifications
- `ws` 8.19.0 for WebSocket

### Issues
1. **Duplicate Redis clients**: Both `ioredis` and `redis` packages are listed. This suggests either a migration in progress or redundant dependencies.
2. **No `node-cron`**: The code comments repeatedly reference needing node-cron (e.g., `index.js` line 31 comment "In production, consider using node-cron"), but it is not in dependencies. All scheduling is done via `setInterval`/`setTimeout`.

---

## SECTION 3: DATABASE CONFIGURATION

**File:** `/home/ccuser/rateright-growth/src/config/database.js` (56 lines)

### Purpose
Creates two Supabase clients: a standard client (anon key, respects Row Level Security) and an admin client (service role key, bypasses RLS).

### Environment Variables Used
- `SUPABASE_URL`
- `SUPABASE_ANON_KEY`
- `SUPABASE_SERVICE_ROLE_KEY`

### Database Tables Referenced Across All Files
From the audited code, the following tables are queried:
- `leads` (core entity)
- `communications` (SMS/call logs)
- `callbacks` (scheduled follow-up calls)
- `conversions`
- `daily_metrics`
- `conversion_patterns`
- `winning_patterns`
- `ai_insights`
- `usage_events`
- `intelligence_daily_metrics`
- `intelligence_suggestions`
- `intelligence_alerts`
- `detected_friction`
- `prompt_versions`
- `prompt_outcomes`
- `quality_audits`
- `weekly_excellence_reports`
- `activation_metrics`
- `stage_transitions`
- `platform_activity`
- `push_subscriptions`
- `notification_preferences`
- `sms_templates`
- `sequences`
- `sequence_steps`
- `sequence_enrollments`
- `objection_responses`

### Issues
1. **No connection pooling or retry logic**: If Supabase becomes temporarily unreachable, the clients are created once at module load. There is no reconnection strategy.
2. **Test connection queries the `leads` table** (line 37): If the `leads` table does not exist or has restrictive RLS, the test will fail even if the connection is fine.
3. **`supabase` (anon) client used in some jobs alongside `supabaseAdmin`**: `refreshCallListScores.js` (line 9) only imports `supabase` (not `supabaseAdmin`). This means it respects RLS and may fail if RLS policies are restrictive. All other jobs use `supabaseAdmin || supabase`.

---

## SECTION 4: MIDDLEWARE AUDIT

### 4.1 requestLogger.js
**File:** `/home/ccuser/rateright-growth/src/middleware/requestLogger.js` (24 lines)

Simple middleware that logs request method, path, response time, status code, and user email via the structured logger. Flags slow requests (>1s) with a warning.

**No issues.**

### 4.2 apiKey.js
**File:** `/home/ccuser/rateright-growth/src/middleware/apiKey.js` (55 lines)

Provides two functions: `requireApiKeyOrAuth` (tries API key first, falls back to JWT) and `requireApiKey` (API key only).

**Environment Variables:** `CLAWDBOT_API_KEY`

**Security Notes:**
- API key comparison is a simple string equality (`apiKey === configuredKey`, line 17). This is NOT timing-safe. For an API key sent in headers, a timing attack is theoretically possible, though the practical risk over HTTP is low.
- The Clawdbot user is hardcoded with `id: 'clawdbot'` (line 19) -- not a real UUID. Any code that assumes `req.user.id` is a valid UUID will break.

### 4.3 rateLimiter.js
**File:** `/home/ccuser/rateright-growth/src/middleware/rateLimiter.js` (249 lines)

Defines 12 rate limiters using `express-rate-limit`. All use in-memory stores (no Redis backing), meaning limits reset on every deployment/restart.

**Rate Limit Summary:**
| Limiter | Window | Max | Key |
|---------|--------|-----|-----|
| globalLimiter | 1 min | 300 | IP |
| aiLimiter | 1 min | 20 | User |
| expensiveAiLimiter | 1 min | 5 | User |
| smsLimiter | 1 hour | 100 | User |
| smsBatchLimiter | 1 hour | 10 | User |
| webhookLimiter | 1 min | 500 | IP |
| dashboardLimiter | 1 min | 100 | User |
| writeLimiter | 1 min | 60 | User |
| deleteLimiter | 1 min | 10 | User |
| importLimiter | 1 hour | 5 | User |
| authLimiter | 1 min | 30 | IP |
| strictLimiter | 1 min | 5 | User |
| trackingLimiter | 1 min | 60 | IP |
| trackingBatchLimiter | 1 min | 10 | IP |

**Issues:**
1. **In-memory rate limiting only** (no Redis store): On each deployment or restart, all rate limit counters reset. Also, if running multiple instances (which Railway can do), limits are per-instance, not shared.
2. **`validate: false`** (line 10): Disables all express-rate-limit validation, suppressing security warnings.
3. **Dead code**: `expensiveAiLimiter`, `smsBatchLimiter`, `deleteLimiter`, `importLimiter`, `authLimiter`, `strictLimiter`, and `trackingBatchLimiter` are exported but NOT used in `index.js`. Only `globalLimiter`, `aiLimiter`, `smsLimiter`, `webhookLimiter`, `dashboardLimiter`, `writeLimiter`, and `trackingLimiter` are mounted. The others may be used inside individual route files.

### 4.4 auth.js
**File:** `/home/ccuser/rateright-growth/src/middleware/auth.js` (140 lines)

**Authentication Flow:**
1. Extract Bearer token from Authorization header
2. Validate token with `supabaseAdmin.auth.getUser(token)`
3. Check email against hardcoded `ALLOWED_EMAILS` whitelist
4. Attach `req.user` with `{id, email, displayName}`

**Security Notes:**
1. **Hardcoded allow-list** (lines 9-25): All authorized users are listed by email in source code. Adding/removing users requires a code deployment. Contains a commented-out revoked user with a date annotation.
2. **ALLOWED_EMAILS exported** (line 138): The list is exported and presumably used elsewhere -- but it means the user list is embedded in code.
3. **optionalAuth** (lines 100-133): Silently ignores all auth errors. If `supabaseAdmin` is down, it just proceeds without user context. This is by design but worth noting.
4. **`displayName` derived from email** (line 83): `user.email.split('@')[0]` -- does not use any user metadata or profile name.

### 4.5 validation.js (middleware)
**File:** `/home/ccuser/rateright-growth/src/middleware/validation.js` (252 lines)

Comprehensive input validation middleware including:
- NoSQL injection prevention via `express-mongo-sanitize`
- XSS stripping via `xss` library
- Request size limiter (10MB max)
- File upload validator (5MB per file, whitelisted MIME types)
- `express-validator` based validation rules
- Custom sanitizers

**Issues:**
1. **NOT mounted globally**: The `securityValidation` chain (line 237-241) and `xssProtection` middleware are defined but NEVER applied in `index.js`. They must be applied in individual route files, meaning protection is inconsistent.
2. **Request size limiter redundant**: Express already has `limit: '1mb'` on json/urlencoded body parsers (index.js line 112-113), while this middleware allows 10MB (line 63). The Express limit of 1MB will reject requests before this middleware's 10MB limit is reached.
3. **Shallow XSS sanitization** (lines 28-56): Only sanitizes top-level string fields in `req.body` and `req.query`. Nested objects are not traversed.
4. **`express-mongo-sanitize` is unnecessary**: The application uses Supabase/PostgreSQL, not MongoDB. This library prevents NoSQL injection patterns (`$gt`, `$where`) that are irrelevant to PostgreSQL.

### 4.6 webhookValidation.js
**File:** `/home/ccuser/rateright-growth/src/middleware/webhookValidation.js` (297 lines)

Validates Twilio and Vapi webhook signatures.

**Issues:**
1. **Vapi validation is speculative** (lines 19-139): The code tries three different HMAC patterns and includes multiple TODO comments admitting it does not know the actual Vapi signature format. In practice, if `VAPI_PUBLIC_KEY` is not set (which it IS set in .env), all Vapi webhooks are accepted WITHOUT validation (lines 30-36).
2. **Vapi allows unsigned webhooks by default** (line 32): Only rejects if `REQUIRE_VAPI_SIGNATURE=true` AND `NODE_ENV=production`. Without both, unsigned webhooks are accepted.
3. **`rawBodyMiddleware` sets encoding** (line 278): `req.setEncoding('utf8')` converts all data to strings, which could cause issues with binary data on the same routes.
4. **Twilio validation delegates to service**: The actual Twilio signature validation (`validateWebhookSignature`) is in `../services/twilio` which was not in scope for this audit.

---

## SECTION 5: UTILITIES AUDIT

### 5.1 validation.js (utils)
**File:** `/home/ccuser/rateright-growth/src/utils/validation.js` (147 lines)

Input validation helpers: UUID validation, Australian phone validation, SMS length calculation, string sanitization, email sanitization.

**Issues:**
1. **UUID regex allows only versions 1-5** (line 12): Version 7 UUIDs (increasingly common) would be rejected by `[1-5]` in the regex. Supabase uses v4 by default, so this is fine for now.
2. **XSS sanitization is basic** (lines 53-61): Only removes angle brackets and `javascript:` protocol, not comprehensive against sophisticated XSS.
3. **Backward compat alias** (line 138): `validateUUID: isValidUUID` -- dead code candidate if nothing uses `validateUUID`.

### 5.2 logger.js
**File:** `/home/ccuser/rateright-growth/src/utils/logger.js` (74 lines)

Structured logging with levels (DEBUG, INFO, WARN, ERROR) and request logging.

**Environment Variables:** `LOG_LEVEL`, `NODE_ENV`

**Issues:**
1. **No log aggregation/rotation**: Writes only to stdout/stderr. No structured JSON output for log aggregation services.
2. **Log level is set at module load**: Cannot be changed at runtime.

### 5.3 fetchWithTimeout.js
**File:** `/home/ccuser/rateright-growth/src/utils/fetchWithTimeout.js` (39 lines)

Wrapper around `fetch()` with AbortController timeout. Default timeout: 30 seconds.

**No significant issues.** Clean implementation.

### 5.4 phone.js
**File:** `/home/ccuser/rateright-growth/src/utils/phone.js` (72 lines)

Australian phone number normalization to E.164 format.

**Issues:**
1. **Accepts non-mobile numbers** (line 33): Numbers starting with `2` or `3` (landlines) are accepted as valid, but the function `isAustralianMobile` (line 61-65) only validates `+614` prefix (mobiles). The normalizer could create E.164 landline numbers that would then fail SMS delivery.

### 5.5 intent.js
**File:** `/home/ccuser/rateright-growth/src/utils/intent.js` (290 lines)

SMS intent classification with buying signal detection for sales prioritization. Classifies messages as positive, negative, identity, question, or unclear.

**Issues:**
1. **Auto-reply messages are hardcoded** (lines 255-271): The identity reply (line 265) contains a specific person's name ("Rocky -- Michael McLoughlin") and business pitch text. This is not configurable.
2. **Regex-based classification limitations**: Short messages or slang may be misclassified. For example, "no" alone is negative, but "no worries" would also match the negative pattern.

---

## SECTION 6: JOBS AUDIT

### 6.1 index.js (jobs scheduler)
**File:** `/home/ccuser/rateright-growth/src/jobs/index.js` (581 lines)

Central job scheduler using `setInterval` and `setTimeout`.

**Active Scheduled Jobs:**

| Job | Interval | Notes |
|-----|----------|-------|
| Platform sync | 15 min | setInterval |
| Critical audit | 15 min | setInterval |
| Hourly audit | 1 hour | setInterval |
| Score recalculation | 1 hour | setInterval |
| Activation metrics | 6 hours | setInterval |
| Impact measurement | 24 hours | setInterval |
| Sequence processor | 1 min | setInterval with job lock |
| Release stale assignments | 1 hour | setInterval |
| Refresh call list scores | 4 hours | setInterval |
| Leaderboard snapshot | 1 hour | setInterval |
| Weekly badges | Friday 3pm AEST | setTimeout chain |
| Daily metrics aggregation | 1am AEST | setTimeout chain |

**Disabled Jobs:**
- `scheduledSms` -- redundant with `sequenceProcessor`
- `callbackReminder` -- "Temporarily disabled for debugging deploy issue"
- Weekly report, daily quality audit, weekly excellence report, monthly deep dive, prompt evolution -- all disabled, replaced by "COO brief system on VPS"

**Issues:**
1. **CRITICAL: AEST timezone calculation ignores daylight saving** (line 47 and many others): All AEST calculations use a fixed UTC+10 offset. During AEDT (daylight saving, October-April), the actual offset is UTC+11. This means all scheduled times will fire 1 hour late during daylight saving months (which is roughly half the year).
2. **No job locking except for sequence processor**: Only `sequenceProcessor` uses `runJobWithLock`. All other jobs could run concurrently if the server restarts during execution or if multiple instances run.
3. **setTimeout chains are fragile**: If a setTimeout callback throws (even though `.catch(console.error)` is used), the rescheduling call might not execute, permanently killing that job.
4. **Dead code**: The functions `msUntilMondayAEST`, `scheduleWeeklyReport`, `msUntilFridayAEST` (partially), `msUntil6amAEST`, `scheduleDailyQualityAudit`, `msUntilMonday715AEST`, `scheduleWeeklyExcellenceReport`, `msUntilFirstOfMonth8amAEST`, `scheduleMonthlyDeepDive`, `msUntil15thOfMonth9amAEST`, `schedulePromptEvolution` are all still defined but commented out from `startJobs()`.
5. **Potential timing conflicts**: Platform sync (15 min) and critical audit (15 min) start at the same time and could hammer the database simultaneously.

### 6.2 backfillHandledBy.js
**File:** `/home/ccuser/rateright-growth/src/jobs/backfillHandledBy.js` (110 lines)

One-time migration job to backfill `handled_by` column from metadata in the `communications` table.

**Issues:**
1. **Tries to execute raw SQL via RPC** (line 21): Calls `db.rpc('exec_sql', { sql: 'ALTER TABLE...' })` which may not exist. Error is silently caught.
2. **Limited to 1000 rows** (line 51): For a full backfill of a large table, this would need multiple runs.
3. **One-at-a-time updates**: Processes records sequentially, no batching.

### 6.3 callbackReminder.js
**File:** `/home/ccuser/rateright-growth/src/jobs/callbackReminder.js` (126 lines)

Currently **DISABLED**. Checks for callbacks due in 15 minutes and sends push notifications.

**Issues:**
1. **Module-level `db` assignment** (line 9): `const db = supabaseAdmin || supabase` evaluated at require time. If the database module hasn't finished initializing, this could be null.
2. **Simple `isRunning` flag** (line 13): Not safe across multiple instances.

### 6.4 dailyMetricsAggregation.js
**File:** `/home/ccuser/rateright-growth/src/jobs/dailyMetricsAggregation.js` (307 lines)

Aggregates `usage_events` into `intelligence_daily_metrics` (page views, flow completion, errors, rage clicks, clicks, unique users, device breakdown).

**Issues:**
1. **Fetches ALL events for a day into memory** (multiple queries, e.g., line 33-38): For high-traffic days, fetching all `usage_events` with `select('page')` without pagination could consume significant memory. No `.limit()` on these queries.
2. **Calls RPC for cleanup** (line 268): `db.rpc('cleanup_old_usage_events')` -- assumes an RPC function exists in Supabase.

### 6.5 promptEvolution.js
**File:** `/home/ccuser/rateright-growth/src/jobs/promptEvolution.js` (408 lines)

**DISABLED** -- was causing "infinite loop spam." Uses AI to analyze underperforming prompt templates and generate improved versions for A/B testing.

**Issues:**
1. **Disabled with dead code left in place** (lines 304-318 in index.js): The `schedulePromptEvolution` function has an early `return` before the actual scheduling code.
2. **No guard against AI-generated prompt quality**: The improved prompt is accepted and deployed to A/B testing without human review.

### 6.6 monthlyDeepDive.js
**File:** `/home/ccuser/rateright-growth/src/jobs/monthlyDeepDive.js` (458 lines)

**DISABLED from scheduler**. Comprehensive monthly analysis sent to Slack.

**Issues:**
1. **Stores monthly report in `weekly_excellence_reports` table** (lines 323-354): Uses the wrong table with a "special marker" (`report_type: 'monthly_deep_dive'`). This is a data modeling smell.

### 6.7 impactMeasurement.js
**File:** `/home/ccuser/rateright-growth/src/jobs/impactMeasurement.js` (392 lines)

Measures impact of implemented AI suggestions by comparing pre/post metrics.

**No significant issues** beyond normal operational complexity.

### 6.8 frictionDetection.js
**File:** `/home/ccuser/rateright-growth/src/jobs/frictionDetection.js` (361 lines)

Detects UX friction points from usage data (abandonment, errors, rage clicks, declining usage).

**Issues:**
1. **Not scheduled**: `runFrictionDetection` is imported in the jobs index but never scheduled via `setInterval` or `setTimeout`. It can only be triggered manually via the `/api/jobs` endpoint.

### 6.9 weeklyRepReport.js
**File:** `/home/ccuser/rateright-growth/src/jobs/weeklyRepReport.js` (199 lines)

Per-rep performance reports sent to Slack.

**Issues:**
1. **Not scheduled**: Imported in jobs index but never added to any interval. Only accessible via manual trigger.

### 6.10 sequenceProcessor.js
**File:** `/home/ccuser/rateright-growth/src/jobs/sequenceProcessor.js` (544 lines)

The most critical active job. Processes automated SMS sequences every 60 seconds.

**Key Features:**
- AEST business hours enforcement (8am-6pm weekdays)
- Send windows (morning 6-7:30am, lunch 12-1pm, knockoff 3-6pm)
- Duplicate protection (30-minute SMS throttle per lead)
- Retry logic with 3 max attempts
- Weekend message rescheduling

**Issues:**
1. **Hardcoded AEST offset** (line 26): `const AEST_OFFSET_MS = 10 * 60 * 60 * 1000` -- same daylight saving problem as the jobs scheduler.
2. **Default sender name hardcoded** (line 155): `'{sender_name}': 'Rocky'` -- not configurable.
3. **Processes only 20 enrollments per run** (line 361): With 60-second intervals, a backlog of 200 enrollments would take ~10 minutes to clear.
4. **No deduplication across restarts**: The job lock (`runJobWithLock`) prevents concurrent runs, but if the server restarts mid-processing, messages could be re-sent.

### 6.11 scheduledSms.js
**File:** `/home/ccuser/rateright-growth/src/jobs/scheduledSms.js` (183 lines)

**DISABLED** -- declared redundant with `sequenceProcessor.js`.

The code is still importable and referenced in the jobs index `runJob` function (line 536), but that path returns an error message.

**Dead code.**

### 6.12 releaseStaleAssignments.js
**File:** `/home/ccuser/rateright-growth/src/jobs/releaseStaleAssignments.js` (119 lines)

Releases leads assigned for more than 24 hours with no activity. Runs every hour.

**Issues:**
1. **N+1 query pattern** (lines 52-75): For each stale lead, it makes a separate query to `communications` to check for recent activity. With 100 stale leads, this is 100 extra database queries.

### 6.13 dailyQualityAudit.js
**File:** `/home/ccuser/rateright-growth/src/jobs/dailyQualityAudit.js` (121 lines)

**DISABLED from scheduler**. Runs full system audit and sends to Slack.

**Issues:**
1. **Lazy Supabase loading** (lines 12-15): Uses a `getSupabase()` function instead of top-level import. This is a pattern inconsistency with other job files.
2. **Graceful handling of missing table** (line 78): Checks for PostgreSQL error code `42P01` (undefined table). Good defensive coding.

### 6.14 frequentAudit.js
**File:** `/home/ccuser/rateright-growth/src/jobs/frequentAudit.js` (347 lines)

Runs critical checks every 15 minutes and external service checks hourly.

**Issues:**
1. **In-memory alert deduplication** (line 27): `const recentAlerts = new Map()` -- resets on every deployment. Could cause alert spam after restarts.
2. **Slack notifications disabled** (lines 153-159): Critical/high severity alerts are commented out with `console.log` instead. Only the `createAlert` DB call happens.
3. **Memory leak potential**: The `recentAlerts` Map is never pruned. If different alert types accumulate over time, the Map grows unboundedly.

### 6.15 weeklyReport.js
**File:** `/home/ccuser/rateright-growth/src/jobs/weeklyReport.js` (326 lines)

**DISABLED from scheduler**. Analyzes conversion patterns and sends insights to Slack.

**Issues:**
1. **Bug in lead query** (line 38): `.not('deleted_at', 'is', null)` fetches leads where `deleted_at IS NOT NULL` -- this returns DELETED leads, not active ones. Should be `.is('deleted_at', null)`.

### 6.16 activationMetrics.js
**File:** `/home/ccuser/rateright-growth/src/jobs/activationMetrics.js` (190 lines)

Calculates activation funnel metrics per lead type (worker vs contractor). Runs every 6 hours.

**Environment Variables:** `ACTIVATION_AVG_REVENUE_WORKER`, `ACTIVATION_AVG_REVENUE_CONTRACTOR`, `ACTIVATION_STUCK_THRESHOLD_DAYS`

**No significant issues.**

### 6.17 dailySummary.js
**File:** `/home/ccuser/rateright-growth/src/jobs/dailySummary.js` (159 lines)

Daily metrics summary sent to Slack. Only runs if `RUN_DAILY_SUMMARY_ON_START=true`.

**No significant issues.**

### 6.18 weeklyExcellenceReport.js
**File:** `/home/ccuser/rateright-growth/src/jobs/weeklyExcellenceReport.js` (208 lines)

**DISABLED from scheduler**. Weekly UX excellence report.

**No significant issues** beyond being dead code.

### 6.19 platformSync.js
**File:** `/home/ccuser/rateright-growth/src/jobs/platformSync.js` (160 lines)

Polls RateRight API every 15 minutes to sync user activity.

**Environment Variables:** `RATERIGHT_API_URL`, `RATERIGHT_ADMIN_API_KEY`

**Issues:**
1. **No timeout on fetch** (line 24): Uses bare `fetch()` without timeout. If the RateRight API hangs, this job blocks indefinitely.
2. **Require inside loop** (line 126): `const { updateLeadScore } = require('../services/scoring')` is called inside a for-loop iteration. Node.js caches requires, so this works but is a code smell.

### 6.20 refreshCallListScores.js
**File:** `/home/ccuser/rateright-growth/src/jobs/refreshCallListScores.js` (127 lines)

Batch-refreshes call list priority scores for stale leads. Runs every 4 hours.

**Issues:**
1. **Uses `supabase` (anon client) instead of `supabaseAdmin`** (line 9): Only imports the anon client. This could fail if RLS policies restrict access. Every other job uses `supabaseAdmin || supabase`.
2. **Individual updates in a loop** (lines 94-112): Updates each lead individually rather than using bulk operations.

---

## SECTION 7: ENVIRONMENT VARIABLES

### Variables Defined in .env
```
SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY
NOTION_API_KEY, NOTION_BUSINESS_STATE_DB, NOTION_TASKS_DB, NOTION_KNOWLEDGE_DB, NOTION_TEAM_DB, NOTION_METRICS_DB
DISABLE_ALL_SMS, SMS_PAUSE_REASON
TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, TWILIO_PHONE_NUMBER, TWILIO_API_KEY_SID, TWILIO_API_KEY_SECRET, TWILIO_TWIML_APP_SID
VAPI_API_KEY, VAPI_PUBLIC_KEY, VAPI_ASSISTANT_ID, VAPI_PHONE_NUMBER_ID
OPENAI_API_KEY
APP_URL, INTERNAL_JOB_KEY
CLAWDBOT_API_KEY
APOLLO_API_KEY
```

### Variables Referenced in Code But NOT in .env
- `PORT` (defaults to 3000)
- `ALLOWED_ORIGINS`
- `NODE_ENV`
- `LOG_LEVEL`
- `DEEPGRAM_API_KEY` (used in services/deepgram, referenced in frequentAudit.js)
- `SLACK_WEBHOOK_URL` (used in services/slack)
- `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY` (push notifications)
- `REQUIRE_VAPI_SIGNATURE`
- `SKIP_WEBHOOK_VALIDATION`
- `RUN_DAILY_SUMMARY_ON_START`, `RUN_QUALITY_AUDIT_ON_START`, `RUN_EXCELLENCE_REPORT_ON_START`, `RUN_METRICS_AGGREGATION_ON_START`, `RUN_MONTHLY_DEEP_DIVE_ON_START`, `RUN_PROMPT_EVOLUTION_ON_START`, `RUN_WEEKLY_REPORT_ON_START`, `RUN_WEEKLY_BADGES_ON_START`
- `ACTIVATION_AVG_REVENUE_WORKER`, `ACTIVATION_AVG_REVENUE_CONTRACTOR`, `ACTIVATION_STUCK_THRESHOLD_DAYS`
- `RATERIGHT_API_URL`, `RATERIGHT_ADMIN_API_KEY`

These are likely set in the Railway deployment environment but not in the local `.env` file.

---

## SECTION 8: SUMMARY OF CRITICAL FINDINGS

### Security Issues (High Priority)

| # | Issue | File | Line(s) |
|---|-------|------|---------|
| 1 | WebSocket endpoints have NO authentication | `src/index.js` | 304-438 |
| 2 | Tracking endpoints unauthenticated, can pollute DB | `src/index.js` | 258-259 |
| 3 | Vapi webhooks accepted without signature validation by default | `src/middleware/webhookValidation.js` | 30-36 |
| 4 | API key comparison not timing-safe | `src/middleware/apiKey.js` | 17 |
| 5 | Hardcoded user allowlist in source code | `src/middleware/auth.js` | 9-25 |
| 6 | Content-Security-Policy disabled globally | `src/index.js` | 94 |

### Bugs

| # | Bug | File | Line |
|---|-----|------|------|
| 1 | `weeklyReport.js` fetches DELETED leads (`.not('deleted_at', 'is', null)`) | `src/jobs/weeklyReport.js` | 38 |
| 2 | AEST timezone ignores daylight saving (UTC+11 in summer) | `src/jobs/index.js` and `sequenceProcessor.js` | Multiple |
| 3 | `refreshCallListScores.js` uses anon client, may fail with RLS | `src/jobs/refreshCallListScores.js` | 9 |

### Dead Code

| # | Description | File |
|---|-------------|------|
| 1 | `scheduledSms.js` -- entirely disabled, replaced by sequenceProcessor | `src/jobs/scheduledSms.js` |
| 2 | Multiple scheduling functions defined but never called | `src/jobs/index.js` |
| 3 | Several rate limiters exported but never mounted | `src/middleware/rateLimiter.js` |
| 4 | `express-mongo-sanitize` -- NoSQL injection prevention for a PostgreSQL app | `src/middleware/validation.js` |
| 5 | Disabled jobs still fully defined: `promptEvolution`, `monthlyDeepDive`, `weeklyExcellenceReport`, `dailyQualityAudit`, `weeklyReport`, `callbackReminder` | Various |

### Architecture Concerns

1. **Rate limiting is in-memory only** -- resets on deployment, not shared across instances.
2. **No proper job scheduler** -- using `setInterval`/`setTimeout` instead of node-cron or a job queue (Bull/BullMQ).
3. **No process-level error handling** -- no `process.on('unhandledRejection')` or `process.on('uncaughtException')` handlers in the entry point.
4. **Validation middleware defined but not globally applied** -- protection depends on individual route files importing and using it.
5. **Duplicate Redis packages** -- both `ioredis` and `redis` in dependencies.