Skip to content

Development Antipatterns

This document catalogs common development antipatterns found in the VitaraVox codebase during the January 2026 review.


1. Placeholder Values in Production Code

Pattern Description

Configuration values with placeholder strings that bypass security controls.

Occurrences Found

Location Placeholder Risk
server.js:54 'your_vapi_webhook_secret_here' Auth bypass
.env.example:15 your_oscar_rest_bridge_api_key Credential exposure
.env.example:21 your_middleware_api_key_here Auth bypass
.env.example:26 your_vapi_webhook_secret Auth bypass
.env.example:32 +1XXXXXXXXXX Transfer failure

Code Example

// PROBLEM: Placeholder enables auth bypass
if (!process.env.VAPI_WEBHOOK_SECRET ||
    process.env.VAPI_WEBHOOK_SECRET === 'your_vapi_webhook_secret_here') {
  console.warn('...skipping signature verification');
  return next();  // ⚠️ SECURITY BYPASS
}

Fix

// SOLUTION: Fail-closed approach
if (!process.env.VAPI_WEBHOOK_SECRET) {
  console.error('FATAL: VAPI_WEBHOOK_SECRET not configured');
  process.exit(1);
}

2. Multiple File Versions

Pattern Description

Multiple versions of the same file exist with unclear canonical source.

Occurrences Found

voice-agent/
├── vapiEndpoints.js           # 29KB - Version 1
├── vapiEndpoints-updated.js   # 35KB - Version 2 (extended)
├── vapiEndpoints-vitara.js    # 6KB  - Version 3 (subset)
└── server.js                  # Imports vapiEndpoints.js

Issues

  • Unclear which version is authoritative
  • Changes may be made to wrong file
  • Dead code increases attack surface
  • Maintenance confusion

Fix

  1. Identify canonical version
  2. Consolidate functionality
  3. Delete obsolete versions
  4. Add version control discipline

3. TODO Comments in Production

Pattern Description

Unfinished implementations marked with TODO comments that never got completed.

Occurrences Found

File Line TODO
vapiEndpoints-updated.js 23 // TODO: Add OSCAR health check
vapiEndpoints.js 831 // TODO: Store in database
vapiEndpoints.js 861 // TODO: Store in PostgreSQL database

Code Example

router.post('/transfer-call', async (req, res) => {
  // Log transfer for analytics
  // TODO: Store in database  ← 🔴 NEVER IMPLEMENTED
  return res.json({ success: true, ... });
});

Fix

Either implement the functionality or remove the dead code path entirely.


4. Debug Logging in Production

Pattern Description

console.log statements throughout production code.

Statistics

Files scanned: 10
Total console.log: 48
Total console.error: 15
Total console.warn: 8
─────────────────────
Total: 71 log statements

Problems

  • PHI Leakage: Patient data may appear in logs
  • Performance: Synchronous I/O overhead
  • Log Noise: Obscures real issues
  • No Levels: Can't filter by severity

Code Examples

// FOUND: Logs potentially sensitive data
console.log(`Searching for patient: "${query}"`);
console.log(`Found ${patients?.length || 0} patient(s)`);
console.log(`Booking appointment:`, appointmentData);

Fix

// SOLUTION: Structured logging with levels
const logger = require('pino')({
  level: process.env.LOG_LEVEL || 'info',
  redact: ['patient.ssn', 'patient.dob', '*.password']
});

logger.info({ query: query.substring(0, 3) + '***' }, 'Patient search');

5. Schema Drift

Pattern Description

Multiple database schema definitions that have diverged.

Occurrences Found

File clinics.id Type Phone Column
database/init.sql SERIAL vapi_phone_number
migrations/001_initial_schema.sql UUID vapi_phone

Additional Differences

-- init.sql (v1)
CREATE TABLE clinics (
  id SERIAL PRIMARY KEY,
  oauth_consumer_key TEXT NOT NULL,
  ...
);

-- 001_initial_schema.sql (v2)
CREATE TABLE clinics (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  ...
);
CREATE TABLE clinic_config (
  oscar_consumer_key VARCHAR(255),  -- Different table!
  ...
);

Fix

  1. Choose canonical schema version
  2. Create migration from v1 to v2
  3. Remove deprecated schema file
  4. Add schema validation in CI

6. Connection Pool Proliferation

Pattern Description

Multiple files create their own database connection pools.

Occurrences Found

// vitaraDb.js
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

// clinicRouter.js
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

// models/Clinic.js
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

// models/User.js
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

// middleware/auth.js (via User.js import)
// ... uses User.js pool

Problems

  • 4x the connections needed
  • No centralized pool management
  • Inconsistent pool configuration
  • Resource exhaustion risk

Fix

// db/pool.js - Single shared pool
const { Pool } = require('pg');

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 5000,
});

module.exports = pool;

// Usage in other files
const pool = require('./db/pool');

7. Inconsistent Error Handling

Pattern Description

Different error response formats across endpoints.

Occurrences Found

// Format A
return res.status(500).json({
  error: 'internal_error',
  message: "I'm having trouble..."
});

// Format B
return res.json({
  success: false,
  error: 'booking_failed',
  message: "I wasn't able to..."
});

// Format C
return res.status(401).json({
  error: 'Missing authentication headers'
});

// Format D
throw error;  // Unhandled, crashes

Fix

Standardize error response format:

// utils/errors.js
class APIError extends Error {
  constructor(code, message, status = 500) {
    super(message);
    this.code = code;
    this.status = status;
  }
}

// Global error handler
app.use((err, req, res, next) => {
  const status = err.status || 500;
  res.status(status).json({
    success: false,
    error: err.code || 'internal_error',
    message: err.message,
    requestId: req.requestId
  });
});

8. Missing Input Validation

Pattern Description

Endpoints accept user input without validation.

Occurrences Found

// No validation on phone number format
const { phone } = req.body;
const normalizedPhone = utils.normalizePhone(phone);
// What if phone is an object? Array? 10MB string?

// No validation on patient data
const { firstName, lastName, dateOfBirth } = req.body;
// SQL injection? XSS? Invalid dates?

// No validation on appointment times
const { startTime, providerId } = req.body;
// Past dates? Invalid providers? Wrong format?

Fix

const Joi = require('joi');

const patientSearchSchema = Joi.object({
  phone: Joi.string().pattern(/^\+?[\d\s\-()]+$/).max(20),
  name: Joi.string().max(100).pattern(/^[a-zA-Z\s'-]+$/),
  firstName: Joi.string().max(50)
});

router.post('/search-patient', async (req, res) => {
  const { error, value } = patientSearchSchema.validate(req.body);
  if (error) {
    return res.status(400).json({
      success: false,
      error: 'validation_error',
      details: error.details
    });
  }
  // Use validated 'value' not raw 'req.body'
});

9. Hardcoded Magic Values

Pattern Description

Configuration values hardcoded throughout codebase.

Occurrences Found

Value Location Purpose
100kb server.js:15 Body parser limit
15000 oscarService.js:79 Request timeout
5 * 60 * 1000 clinicRouter.js:13 Cache TTL
300000 server.js:69 Replay attack window
90 vapiEndpoints.js:502 Days ahead for search
5 vapiEndpoints.js:517 Max providers to query
12 User.js:10 Bcrypt salt rounds

Fix

// config/constants.js
module.exports = {
  BODY_LIMIT: process.env.BODY_LIMIT || '100kb',
  OSCAR_TIMEOUT_MS: parseInt(process.env.OSCAR_TIMEOUT_MS) || 15000,
  CACHE_TTL_MS: parseInt(process.env.CACHE_TTL_MS) || 300000,
  REPLAY_WINDOW_MS: 300000,
  APPOINTMENT_SEARCH_DAYS: 90,
  MAX_PROVIDER_QUERIES: 5,
  BCRYPT_SALT_ROUNDS: 12
};

10. In-Memory State

Pattern Description

Application state stored in memory that won't survive restarts or scale across instances.

Occurrences Found

// middleware/auth.js
const loginAttempts = new Map();  // ⚠️ Lost on restart

// clinicRouter.js
const clinicCache = new Map();  // ⚠️ Not shared across instances

Problems

  • Lost on process restart
  • Not shared in multi-instance deployments
  • Memory leak potential (no size limits)
  • No persistence

Fix

// Use Redis for shared state
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);

async function recordLoginAttempt(ip) {
  const key = `login:${ip}`;
  const attempts = await redis.incr(key);
  await redis.expire(key, 900); // 15 minutes
  return attempts;
}

Summary

Antipattern Count Severity Effort to Fix
Placeholder values 5 🔴 Critical 2 hours
Multiple file versions 3 🟠 High 4 hours
TODO comments 4 🟡 Medium 2 hours
Debug logging 71 🟡 Medium 4 hours
Schema drift 2 🟠 High 8 hours
Pool proliferation 4 🟡 Medium 2 hours
Inconsistent errors 12 🟡 Medium 4 hours
Missing validation 15 🔴 Critical 8 hours
Hardcoded values 8 🔵 Low 2 hours
In-memory state 2 🟡 Medium 4 hours

Total Estimated Remediation: ~40 hours


Document generated: January 2026