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¶
- Identify canonical version
- Consolidate functionality
- Delete obsolete versions
- 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¶
- Choose canonical schema version
- Create migration from v1 to v2
- Remove deprecated schema file
- 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