Review Date: January 3, 2026
Reviewed By: Senior Engineering Team (Automated Analysis)
Commit: 67c539e on main branch
This comprehensive architecture review identifies 47 issues across 7 categories, with 8 critical, 15 high, 18 medium, and 6 low priority findings. The most pressing concerns are:
- Security vulnerabilities in admin routes (command injection risk, default credentials)
- No authentication on public API endpoints despite project requirements
- Tight coupling and lack of dependency injection making the codebase hard to test
- Database schema drift between TypeORM entities and migrations
- Test coverage at 24% vs the 80% threshold, with database tests entirely skipped
- Critical Issues
- Architecture & Design Patterns
- Database & ORM Patterns
- Error Handling & Reliability
- API Design & REST Patterns
- Security Vulnerabilities
- Code Quality Issues
- Testing Gaps
- Recommendations Summary
These issues require immediate attention before the next production deployment.
Severity: CRITICAL
File: src/routes/admin.routes.ts
Lines: 22, 56, 63, 70
const output = execSync('npx typeorm migration:run -d ormconfig.js', { encoding: 'utf8' });
execSync('node dist/scripts/import-rcl-with-dates.js', { encoding: 'utf8' });Risk: Command injection, event loop blocking, no timeout handling.
Remediation: Replace with programmatic alternatives or use a job queue (Bull, Agenda).
Severity: CRITICAL
File: src/routes/admin.routes.ts
Line: 8
const ADMIN_KEY = process.env.ADMIN_KEY || 'default-admin-key-change-me';Risk: If ADMIN_KEY is not set, anyone can access admin endpoints with the known default.
Remediation:
const ADMIN_KEY = process.env.ADMIN_KEY;
if (!ADMIN_KEY) {
throw new Error('ADMIN_KEY environment variable is required');
}Severity: CRITICAL
Files: All route files in src/routes/
Despite CLAUDE.md stating "all endpoints must be authenticated", no authentication middleware is applied to /readings, /traditions, or /calendar endpoints.
Remediation: Implement API key or JWT middleware for all non-health endpoints.
Severity: CRITICAL
Entity: src/models/special-day.entity.ts (lines 107-111)
Migration: None creates this column
The SpecialDay entity expects tradition_id but no migration creates it. This was causing production 500 errors (fixed in commit 67c539e).
Remediation: Audit all entities against migrations. Create comprehensive sync migration.
Severity: CRITICAL
File: src/config/database.config.ts
Line: 15
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false,Risk: Man-in-the-middle attacks on database connections.
Remediation: Set rejectUnauthorized: true in production with proper CA certificates.
Severity: HIGH Impact: Tight coupling, difficult testing, no lifecycle management
Current Pattern:
// src/controllers/readings.controller.ts:8-10
constructor() {
this.readingsService = new ReadingsService(); // Direct instantiation
}Recommendation: Implement a DI container (tsyringe, inversify, or awilix).
Severity: HIGH
File: src/services/database.service.ts
export class DatabaseService {
private static dataSource: DataSource;
public static getDataSource(): DataSource { ... }
}Issues:
- Global mutable state
- Cannot run parallel tests with different configs
- Hard to mock
Recommendation: Convert to injectable service with interface.
Severity: MEDIUM
File: src/app.ts
Lines: 113-328
220+ lines of inline HTML for the landing page violates separation of concerns.
Recommendation: Extract to template file or static HTML.
Severity: MEDIUM
File: src/controllers/readings.controller.ts
Lines: 47-98
The getToday method builds complex response structures that belong in the service layer.
Recommendation: Move response shaping to service; controllers should only handle HTTP concerns.
Severity: MEDIUM Files: All route files
let readingsController: ReadingsController | null = null;
function getController(): ReadingsController {
if (!readingsController) {
readingsController = new ReadingsController();
}
return readingsController;
}This pattern is repeated in all routes as a workaround for timing issues.
Recommendation: Use proper application bootstrapping with DI.
Severity: MEDIUM
Services directly use TypeORM QueryBuilder, mixing data access with business logic.
Recommendation: Introduce repository pattern:
src/repositories/
readings.repository.ts
traditions.repository.ts
Severity: HIGH
File: src/services/readings.service.ts
Multiple tradition lookups per request:
// Lines 67-91 - Up to 2 sequential queries per tradition lookup
let tradition = await this.traditionRepository.findOne({...});
if (!tradition) {
tradition = await this.traditionRepository.findOne({...}); // Second query
}Impact: Every reading request performs 2+ tradition lookups.
Recommendation: Cache tradition lookups or use single OR query.
Severity: HIGH
| Entity Field | Migration Column | Issue |
|---|---|---|
SpecialDay.traditionId |
Not created | Missing column |
SpecialDay.rank (enum) |
rank (integer/varchar) |
Type mismatch |
Reading.readingOffice (enum) |
reading_office (varchar) |
Type mismatch |
Recommendation: Create comprehensive migration to align schema with entities.
Severity: MEDIUM
Common query patterns lack optimized indexes:
| Query Pattern | Missing Index |
|---|---|
WHERE date = ? AND traditionId = ? AND readingOffice = ? |
['date', 'traditionId', 'readingOffice'] |
WHERE traditionId = ? AND year = ? |
['traditionId', 'year'] |
Severity: LOW
File: src/models/tradition.entity.ts
Three indexes on the same name column:
idx_tradition_name(class decorator)- Implicit unique constraint index
idx_tradition_name_unique(column decorator)
Severity: MEDIUM
File: src/services/database.service.ts
Single-attempt initialization fails immediately in containerized environments.
Recommendation: Add exponential backoff retry logic.
Severity: MEDIUM
- Deleting
Traditioncascades removeReadingrecords - Deleting
LiturgicalYeardoes NOT cascade removeReadingrecords
This inconsistency can lead to orphaned data.
Severity: HIGH
File: src/services/readings.service.ts
// Lines 156-159
} catch (error) {
logger.error(`Error fetching readings for date ${date}:`, error);
return null; // Silent failure - client gets "not found" instead of "error"
}Pattern repeats in:
findReadingsByLiturgicalContext(returns empty array)getByDateRange(returns empty result)getReadingsByProper(returns null)getDailyOfficeReadings(returns null)
Impact: Database failures appear as "no data found" to clients.
Recommendation: Throw errors; let controllers decide response.
Severity: MEDIUM
Two separate loggers:
src/utils/logger.ts- Simple Winston loggersrc/observability/logger.ts- Full observability logger
Different files import different loggers, causing inconsistent logging.
Recommendation: Consolidate to single logger.
Severity: MEDIUM
Service error logs don't include correlation IDs or request context.
logger.error(`Error fetching readings for date ${date}:`, error);
// Missing: requestId, userId, traceIdSeverity: MEDIUM
Four different error response formats exist:
- Error handler:
{ error: { message, statusCode, timestamp } } - 404 handler: Duplicates
messageandtimestampat root - Rate limiter:
{ error, message, retryAfter, remaining } - Admin routes:
{ error: string }
Recommendation: Standardize to single format.
Severity: MEDIUM
File: src/index.ts
Missing process.on('unhandledRejection') handler.
Severity: HIGH
File: src/routes/admin.routes.ts
| Endpoint | Current | Should Be |
|---|---|---|
/debug-readings |
POST | GET |
/query-readings-by-date |
POST | GET |
/fix-rcl-proper |
POST | DELETE |
/fix-duplicate-traditions |
POST | PATCH |
Severity: MEDIUM
Different endpoints use different response structures:
// Pattern 1: Standard
{ data: {...}, timestamp: "..." }
// Pattern 2: With total
{ data: [...], total: 5, timestamp: "..." }
// Pattern 3: Flat (readings/today)
{ date: "...", tradition: "...", lectionary: {...}, data: {...} }
// Pattern 4: Admin
{ success: true, message: "...", results: [...] }Severity: MEDIUM
The entire /api/v1/admin/* router has no Swagger annotations.
Severity: MEDIUM
Readingschema referenced but not definedSearchtag defined but no endpoints use it- Security schemes defined but not applied to endpoints
Severity: LOW
- Only
/readings/rangesupports pagination - Some endpoints return
totalat root, others inpaginationobject - No cursor-based pagination for large datasets
Severity: HIGH
File: src/routes/admin.routes.ts
const { key } = req.body;
if (key !== ADMIN_KEY) { ... }Issues:
- Visible in access logs
- May be cached by proxies
- Not using constant-time comparison (timing attack)
Recommendation: Use Authorization header with constant-time comparison.
Severity: HIGH
File: src/middleware/error-handler.ts
body: req.body, // May contain admin key
params: req.params,
query: req.query,Recommendation: Redact sensitive fields before logging.
Severity: HIGH
File: src/config/database.config.ts
username: process.env.DB_USERNAME || 'postgres',
password: process.env.DB_PASSWORD || 'password',Severity: MEDIUM
express-validator is installed but not used. No runtime schema validation for request bodies.
Severity: MEDIUM
Admin endpoints use same generic rate limit as public API.
| Code | Files | Recommendation |
|---|---|---|
formatDate() helper |
calendar.service.ts, traditions.service.ts |
Extract to utils/date.ts |
| Tradition lookup logic | 3 services | Create TraditionLookupService |
LiturgicalColor enum |
season.entity.ts, liturgical-calendar.ts |
Use single source |
| Easter calculation | 3 files | Consolidate in one service |
| Logger implementation | 2 files | Use single logger |
| Code | File | Action |
|---|---|---|
ApiResponse<T> interface |
lectionary.types.ts |
Remove (never imported) |
PaginatedResponse<T> |
lectionary.types.ts |
Remove (never imported) |
ErrorResponse |
lectionary.types.ts |
Remove (never imported) |
| Debug scripts | src/scripts/debug-*.ts |
Move to tests or remove |
force-rcl-fix.js |
src/scripts/ |
Remove (one-time fix) |
Severity: MEDIUM
Extensive use of any type:
const response: any = { ... };
const readings = dailyOfficeReadings.readings.filter((r: any) => ...);File: src/services/calendar.service.ts:42
// TODO: Implement database query| Metric | Current | Target | Gap |
|---|---|---|---|
| Overall | 23.82% | 80% | -56.18% |
| Statements | 24.89% | 80% | -55.11% |
| Branches | 18.11% | 80% | -61.89% |
| Functions | 20.37% | 80% | -59.63% |
| File | Coverage | Risk |
|---|---|---|
database.service.ts |
0% (skipped) | CRITICAL |
health-checks.ts |
0% | CRITICAL |
admin.routes.ts |
8.69% | HIGH |
rate-limiter.ts |
0% | HIGH |
observability.ts |
0% | HIGH |
- Over-mocking: Global mock in
setup.tsreturns hardcoded data, hiding query bugs - No database tests: All tests mock the database
- No E2E tests: Missing workflow tests
- Timezone-sensitive tests: Date mocking can fail in certain timezones
| # | Issue | Effort | Impact |
|---|---|---|---|
| 1 | Remove default admin key | 15 min | Critical |
| 2 | Enable SSL cert validation | 15 min | Critical |
| 3 | Move admin key to header | 30 min | High |
| 4 | Add authentication middleware | 2-4 hrs | Critical |
| # | Issue | Effort | Impact |
|---|---|---|---|
| 5 | Replace execSync with job queue |
4-8 hrs | Critical |
| 6 | Create schema sync migration | 4 hrs | High |
| 7 | Implement DI container | 8-16 hrs | High |
| 8 | Consolidate duplicate code | 4 hrs | Medium |
| 9 | Standardize error responses | 4 hrs | Medium |
| 10 | Add missing indexes | 2 hrs | Medium |
| # | Issue | Effort | Impact |
|---|---|---|---|
| 11 | Increase test coverage to 80% | 40+ hrs | High |
| 12 | Add repository layer | 16 hrs | Medium |
| 13 | Document admin API in Swagger | 4 hrs | Medium |
| 14 | Implement request validation | 8 hrs | Medium |
| 15 | Add database integration tests | 16 hrs | Medium |
| # | Issue | Effort | Impact |
|---|---|---|---|
| 16 | Add E2E test suite | 24+ hrs | Medium |
| 17 | Implement cursor pagination | 8 hrs | Low |
| 18 | Add secret rotation capability | 8 hrs | Low |
src/
├── app.ts
├── index.ts
├── config/
│ ├── database.config.ts
│ └── swagger.ts
├── controllers/
│ ├── calendar.controller.ts
│ ├── readings.controller.ts
│ └── traditions.controller.ts
├── middleware/
│ ├── error-handler.ts
│ ├── health-checks.ts
│ ├── observability.ts
│ ├── rate-limiter.ts
│ └── request-logger.ts
├── models/
│ ├── liturgical-year.entity.ts
│ ├── reading.entity.ts
│ ├── season.entity.ts
│ ├── special-day.entity.ts
│ └── tradition.entity.ts
├── routes/
│ ├── admin.routes.ts
│ ├── calendar.routes.ts
│ ├── readings.routes.ts
│ └── traditions.routes.ts
├── services/
│ ├── calendar.service.ts
│ ├── database.service.ts
│ ├── liturgical-calendar.service.ts
│ ├── readings.service.ts
│ └── traditions.service.ts
├── migrations/ (7 files)
├── scripts/ (15+ files)
└── tests/ (16 files)
This report was generated by automated code analysis. Manual review is recommended for security-critical issues.