44 KiB
Common Errors and Solutions
This guide covers the most frequently encountered errors in Changemaker Lite V2 and their solutions.
Overview
How to Use This Guide
- Find your error - Use the error code or message to locate the section
- Diagnose - Read the symptoms and causes
- Apply solution - Follow step-by-step instructions
- Prevent recurrence - Implement preventive measures
Error Severity Levels
| Level | Icon | Meaning | Action |
|---|---|---|---|
| Critical | 🔴 | System down or data at risk | Fix immediately |
| High | 🟠 | Feature unavailable | Fix within hours |
| Medium | 🟡 | Degraded performance | Fix within days |
| Low | 🟢 | Minor inconvenience | Fix when convenient |
Quick Error Lookup
| Error Code | Category | Page |
|---|---|---|
| 401 | Authentication | Link |
| 403 | Authorization | Link |
| 404 | Not Found | Link |
| 422 | Validation | Link |
| 500 | Server Error | Link |
| CORS | Frontend | Link |
| ECONNREFUSED | Database | Link |
Authentication Errors
401 Unauthorized
Severity: 🟠 High
Symptoms
{
"error": "Unauthorized",
"message": "Invalid or missing token"
}
Browser console:
Error: Request failed with status code 401
Common Causes
- Missing token - No Authorization header sent
- Expired token - Access token older than 15 minutes
- Invalid token - Corrupted or tampered token
- Wrong environment - Token from dev used in production
Solutions
Solution 1: Check if logged in
// In browser console
console.log(localStorage.getItem('auth-storage'));
If null or missing accessToken, you need to log in again.
Solution 2: Refresh token
The frontend automatically refreshes tokens. If this fails:
- Log out completely
- Clear localStorage:
localStorage.clear() - Log in again
Solution 3: Verify API configuration
Check admin/.env:
VITE_API_URL=http://localhost:4000 # Must match actual API URL
Solution 4: Check token expiration
// In browser console
const storage = JSON.parse(localStorage.getItem('auth-storage'));
const payload = JSON.parse(atob(storage.state.accessToken.split('.')[1]));
console.log('Token expires:', new Date(payload.exp * 1000));
console.log('Current time:', new Date());
If expired, the refresh interceptor should handle this. If not working:
# Check API logs
docker compose logs api | grep "refresh"
Prevention
- Auto-refresh works - Frontend handles token refresh automatically
- Long sessions - Refresh tokens valid for 7 days
- Activity-based - Tokens refresh on API calls
- Clear error handling - Frontend redirects to login on failure
!!! warning "Security Note" 401 errors may return generic messages to prevent user enumeration. This is intentional security behavior.
403 Forbidden
Severity: 🟠 High
Symptoms
{
"error": "Forbidden",
"message": "Insufficient permissions"
}
Or role-specific:
{
"error": "Forbidden",
"message": "Requires one of: SUPER_ADMIN, MAP_ADMIN"
}
Common Causes
- Wrong role - User lacks required role
- TEMP user - Temporary users restricted from most features
- Feature disabled - Feature flag not enabled
- Wrong endpoint - Using admin endpoint as public user
Solutions
Solution 1: Check user role
# In database
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT email, role FROM \"User\" WHERE email = 'your@email.com';"
Solution 2: Update user role
-- Via Prisma Studio (recommended)
docker compose exec api npx prisma studio
-- Navigate to User table, edit role
-- Or via SQL
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "UPDATE \"User\" SET role = 'MAP_ADMIN' WHERE email = 'your@email.com';"
Solution 3: Check feature flags
# In API logs
docker compose logs api | grep "ENABLE_"
# Check .env
cat .env | grep ENABLE
Solution 4: Verify endpoint permissions
Check api/src/modules/*/routes.ts:
// Admin endpoint
router.post('/', authenticate, requireRole('SUPER_ADMIN'), ...);
// Public endpoint (no auth)
router.get('/public', ...);
Prevention
- Role-based access control - Clear role hierarchy
- Explicit permissions - Each endpoint lists required roles
- Audit trail - Track permission changes
- Documentation - Role matrix in Access Control
Invalid Token
Severity: 🟠 High
Symptoms
{
"error": "Unauthorized",
"message": "Invalid token"
}
Or in API logs:
Error: jwt malformed
Error: invalid signature
Error: jwt must be provided
Common Causes
- Corrupted token - LocalStorage corruption
- Wrong secret - JWT_ACCESS_SECRET changed
- Modified token - Attempted tampering
- Format error - Not a valid JWT structure
Solutions
Solution 1: Clear and re-login
// In browser console
localStorage.clear();
// Then log in again
Solution 2: Verify JWT structure
Valid JWT has 3 parts separated by dots:
const token = 'header.payload.signature';
console.log(token.split('.').length); // Should be 3
Solution 3: Check secret configuration
# In .env
JWT_ACCESS_SECRET=your-secret-here-32-chars-min
JWT_REFRESH_SECRET=different-secret-here-32-chars-min
# Secrets must:
# - Be different from each other
# - Be at least 32 characters
# - Remain unchanged (changing invalidates all tokens)
Solution 4: Verify token in logs
# API logs show token validation errors
docker compose logs api | tail -100 | grep "jwt"
Prevention
- Secure secrets - Use
openssl rand -hex 32 - Never commit secrets - Keep in .env (gitignored)
- Rotate carefully - Changing secrets logs out all users
- Monitor errors - Alert on spike in invalid token errors
Token Expired
Severity: 🟡 Medium
Symptoms
{
"error": "Unauthorized",
"message": "Token expired"
}
Or:
Error: jwt expired
Common Causes
- Access token expired - Normal after 15 minutes of inactivity
- Refresh token expired - Refresh token older than 7 days
- System clock skew - Server/client time mismatch
- Refresh failed - Refresh token invalid or revoked
Solutions
Solution 1: Automatic refresh
Frontend automatically refreshes tokens on 401. If this fails:
// Check refresh token in localStorage
const storage = JSON.parse(localStorage.getItem('auth-storage'));
console.log('Has refresh token:', !!storage?.state?.refreshToken);
Solution 2: Manual login
If refresh token expired (after 7 days):
- You'll be redirected to login automatically
- Log in with email/password
- New tokens issued
Solution 3: Check system time
# On server
date
# Sync if incorrect
sudo ntpdate -s time.nist.gov
Solution 4: Verify token expiration
# In API logs
docker compose logs api | grep "expired"
# Check token age
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT email, \"createdAt\", \"expiresAt\" FROM \"RefreshToken\" ORDER BY \"createdAt\" DESC LIMIT 10;"
Prevention
- Sliding sessions - Tokens auto-refresh on activity
- Long refresh window - 7-day refresh token validity
- Graceful handling - Automatic re-login redirect
- Activity tracking - Monitor token refresh patterns
!!! tip "Developer Tip"
During development, use longer token expiration in .env:
bash JWT_ACCESS_EXPIRATION=1d # Instead of 15m JWT_REFRESH_EXPIRATION=30d # Instead of 7d
User Not Found
Severity: 🟡 Medium
Symptoms
{
"error": "Unauthorized",
"message": "Invalid credentials"
}
Note: Same message for both "user not found" and "wrong password" (security feature).
Common Causes
- Wrong email - Typo in email address
- User deleted - Account removed from database
- Wrong database - Connected to wrong environment
- Case sensitivity - Email stored differently
Solutions
Solution 1: Verify user exists
# Check database
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT email, role FROM \"User\" WHERE email ILIKE '%search%';"
Solution 2: Check email format
Emails are stored lowercase:
-- Find user case-insensitive
SELECT * FROM "User" WHERE LOWER(email) = LOWER('User@Example.com');
Solution 3: Create user if missing
# Via API
curl -X POST http://localhost:4000/api/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "SecurePass123!",
"name": "User Name"
}'
# Or via admin UI at /app/users
Solution 4: Check database connection
# Verify correct database
docker compose exec api npx prisma db pull
# Check DATABASE_URL in .env
cat .env | grep DATABASE_URL
Prevention
- Email validation - Enforce valid email format
- Case normalization - Store emails lowercase
- Soft deletes - Consider flagging instead of deleting
- Audit trail - Log user deletions
API Errors
500 Internal Server Error
Severity: 🔴 Critical
Symptoms
{
"error": "Internal Server Error",
"message": "An unexpected error occurred"
}
Or frontend error:
Error: Request failed with status code 500
Common Causes
- Unhandled exception - Code threw unexpected error
- Database error - Query failed
- Missing environment variable - Required config missing
- Type error - Runtime type mismatch
Solutions
Solution 1: Check API logs
# View recent logs
docker compose logs api --tail=100
# Follow logs in real-time
docker compose logs -f api
# Search for errors
docker compose logs api | grep -i error | tail -20
Solution 2: Common error patterns
// Missing environment variable
Error: SMTP_HOST is required
// Solution: Add to .env
// Database connection error
Error: Can't reach database server at `v2-postgres:5432`
// Solution: Check database is running
// Type error
TypeError: Cannot read property 'id' of undefined
// Solution: Check code for null checks
Solution 3: Restart API
# Restart API container
docker compose restart api
# Or rebuild if code changed
docker compose up -d --build api
Solution 4: Enable debug logging
# In .env
LOG_LEVEL=debug
# Restart API
docker compose restart api
# Check detailed logs
docker compose logs api
Prevention
- Error handling - Try/catch in all routes
- Input validation - Validate all inputs with Zod
- Type safety - Use TypeScript strictly
- Health checks - Monitor API health
- Alerting - Set up alerts for 500 errors
!!! warning "Production Alert" 500 errors indicate bugs. Always investigate and fix root cause.
400 Bad Request
Severity: 🟡 Medium
Symptoms
{
"error": "Bad Request",
"message": "Invalid request format"
}
Or with validation details:
{
"error": "Bad Request",
"message": "Validation failed: 2 errors"
}
Common Causes
- Invalid JSON - Malformed request body
- Wrong Content-Type - Missing or incorrect header
- Missing required field - Required parameter not sent
- Invalid data type - String sent for number field
Solutions
Solution 1: Check request format
// Correct format
const response = await api.post('/api/users', {
email: 'user@example.com',
password: 'SecurePass123!',
name: 'User Name'
}, {
headers: {
'Content-Type': 'application/json'
}
});
// Common mistakes:
// ❌ Missing Content-Type header
// ❌ Sending FormData to JSON endpoint
// ❌ Malformed JSON (trailing comma, unquoted keys)
Solution 2: Validate against schema
Check API schema in api/src/modules/*/schemas.ts:
// Example: User creation schema
export const createUserSchema = z.object({
email: z.string().email(),
password: z.string().min(12),
name: z.string().min(1),
role: z.enum(['USER', 'MAP_ADMIN', 'INFLUENCE_ADMIN', 'SUPER_ADMIN']).optional()
});
Solution 3: Check API logs for details
# Logs show validation errors
docker compose logs api | grep "Validation failed"
# Example output:
# Validation failed: {
# "email": "Invalid email format",
# "password": "Must be at least 12 characters"
# }
Solution 4: Test with curl
# Test request
curl -X POST http://localhost:4000/api/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"email": "test@example.com",
"password": "SecurePass123!",
"name": "Test User"
}'
Prevention
- Client-side validation - Validate before sending
- TypeScript types - Use generated types from API
- Schema documentation - Document all endpoints
- Error messages - Clear validation error messages
404 Not Found
Severity: 🟢 Low to 🟡 Medium
Symptoms
{
"error": "Not Found",
"message": "Resource not found"
}
Or specific:
{
"error": "Not Found",
"message": "Campaign not found"
}
Common Causes
- Wrong ID - Resource doesn't exist
- Wrong URL - Typo in endpoint path
- Deleted resource - Resource was deleted
- Wrong HTTP method - GET instead of POST
Solutions
Solution 1: Verify resource exists
# Check database
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT id, name FROM \"Campaign\" WHERE id = 'YOUR_ID';"
Solution 2: Check URL format
// Correct formats
GET /api/campaigns/:id // Single campaign
GET /api/campaigns // List campaigns
POST /api/campaigns // Create campaign
PUT /api/campaigns/:id // Update campaign
DELETE /api/campaigns/:id // Delete campaign
// Common mistakes:
// ❌ /api/campaign/:id (singular, should be plural)
// ❌ /api/campaigns/id/:id (extra 'id/' in path)
// ❌ /api/campaign (wrong singular/plural)
Solution 3: Check route registration
# API logs show registered routes on startup
docker compose logs api | grep "Registered route"
# Or check routes file
cat api/src/modules/*/routes.ts
Solution 4: Test endpoint
# List all campaigns to verify endpoint
curl http://localhost:4000/api/campaigns
# Test specific ID
curl http://localhost:4000/api/campaigns/YOUR_ID
Prevention
- UUID validation - Validate ID format before querying
- Soft deletes - Flag as deleted instead of removing
- Resource existence checks - Verify before operations
- Clear error messages - Specify which resource not found
422 Unprocessable Entity
Severity: 🟡 Medium
Symptoms
{
"error": "Unprocessable Entity",
"message": "Validation failed",
"details": {
"email": "Email already exists",
"password": "Must contain uppercase, lowercase, and digit"
}
}
Common Causes
- Business logic violation - Email already exists
- Data integrity - Foreign key doesn't exist
- Complex validation - Password requirements not met
- State conflict - Can't delete resource in use
Solutions
Solution 1: Read validation details
The details field shows exactly what's wrong:
try {
await api.post('/api/users', userData);
} catch (error) {
if (error.response?.status === 422) {
console.log('Validation errors:', error.response.data.details);
// Show to user field-by-field
}
}
Solution 2: Check constraints
# Email uniqueness
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT email FROM \"User\" WHERE email = 'test@example.com';"
# Foreign key exists
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT id FROM \"Campaign\" WHERE id = 'CAMPAIGN_ID';"
Solution 3: Fix data
Common fixes:
// Email already exists → Use different email
email: 'newuser@example.com'
// Password too weak → Meet requirements
password: 'SecurePass123!' // 12+ chars, upper, lower, digit
// Foreign key missing → Create parent first
// Create campaign before creating email
// Resource in use → Delete dependents first
// Delete locations before deleting cut
Solution 4: Check database schema
# View constraints
docker compose exec api npx prisma studio
# Navigate to model, see unique fields and relations
Prevention
- Client validation - Check constraints before submitting
- Clear requirements - Document validation rules
- Helpful messages - Explain how to fix
- Cascade deletes - Auto-delete dependents when safe
Database Errors
Connection Refused
Severity: 🔴 Critical
Symptoms
Error: connect ECONNREFUSED 127.0.0.1:5433
Or:
Error: Can't reach database server at `v2-postgres:5432`
Common Causes
- Database not running - Container stopped
- Wrong connection string - Incorrect host/port
- Network issue - Container can't reach database
- Port conflict - Port already in use
Solutions
Solution 1: Check database status
# List running containers
docker compose ps
# Database should show as "running"
# If not:
docker compose up -d v2-postgres
Solution 2: Verify connection string
# Check .env
cat .env | grep DATABASE_URL
# Should be (from API container):
DATABASE_URL="postgresql://changemaker:PASSWORD@v2-postgres:5432/changemaker_v2"
# Or (from host):
DATABASE_URL="postgresql://changemaker:PASSWORD@localhost:5433/changemaker_v2"
Solution 3: Check database logs
# View database logs
docker compose logs v2-postgres
# Look for:
# - "database system is ready to accept connections" (good)
# - "FATAL: password authentication failed" (bad - wrong password)
# - "port 5432 already in use" (bad - port conflict)
Solution 4: Test connection manually
# From host
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 -c "SELECT NOW();"
# Should return current timestamp
# If fails, database isn't running properly
Solution 5: Restart database
# Restart database container
docker compose restart v2-postgres
# Or recreate if corrupted
docker compose down v2-postgres
docker compose up -d v2-postgres
# Wait for "ready to accept connections" message
docker compose logs -f v2-postgres
Prevention
- Health checks - Monitor database availability
- Auto-restart - Configure restart policy
- Connection pooling - Handle transient failures
- Alerting - Alert on connection failures
Too Many Connections
Severity: 🟠 High
Symptoms
Error: too many connections for database "changemaker_v2"
Or:
Error: Prepared statement "prisma_xxx" already exists
Common Causes
- Connection leak - Connections not released
- Pool too small - Not enough connections for load
- Long-running queries - Blocking connections
- Multiple clients - Too many Prisma instances
Solutions
Solution 1: Check active connections
# View connections
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT count(*) FROM pg_stat_activity WHERE datname = 'changemaker_v2';"
# PostgreSQL default max: 100 connections
# Prisma default pool: 10 connections
Solution 2: Kill idle connections
-- Find idle connections
SELECT pid, usename, state, query_start
FROM pg_stat_activity
WHERE datname = 'changemaker_v2' AND state = 'idle';
-- Kill specific connection
SELECT pg_terminate_backend(PID_HERE);
-- Kill all idle connections (careful!)
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE datname = 'changemaker_v2' AND state = 'idle';
Solution 3: Adjust connection pool
In api/prisma/schema.prisma:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
// Add connection pool config
// connectionLimit = 10 // Default
}
Or via DATABASE_URL:
DATABASE_URL="postgresql://user:pass@host:5432/db?connection_limit=20"
Solution 4: Restart API
# Restart releases all connections
docker compose restart api
# Check if connections cleared
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT count(*) FROM pg_stat_activity WHERE datname = 'changemaker_v2';"
Solution 5: Increase PostgreSQL max connections
In docker-compose.yml:
v2-postgres:
# ...
command: postgres -c max_connections=200
Then restart:
docker compose up -d v2-postgres
Prevention
- Proper cleanup - Always close Prisma clients
- Connection pooling - Use appropriate pool size
- Monitor connections - Alert on high usage
- Query optimization - Reduce long-running queries
Unique Constraint Violation
Severity: 🟡 Medium
Symptoms
Error: Unique constraint failed on the fields: (`email`)
Or:
PrismaClientKnownRequestError:
Unique constraint failed on the constraint: `User_email_key`
Common Causes
- Duplicate email - User already exists
- Race condition - Two creates at same time
- Case sensitivity - Email differs only in case
- Retry logic - Request sent multiple times
Solutions
Solution 1: Check existing records
# Find duplicate
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT id, email, \"createdAt\" FROM \"User\" WHERE email = 'duplicate@example.com';"
Solution 2: Update instead of create
// Instead of:
await prisma.user.create({ data: { email, ... } });
// Use upsert:
await prisma.user.upsert({
where: { email },
update: { name, ... },
create: { email, name, ... }
});
Solution 3: Handle error gracefully
try {
await prisma.user.create({ data });
} catch (error) {
if (error.code === 'P2002') {
// Unique constraint violation
const field = error.meta?.target?.[0];
throw new Error(`${field} already exists`);
}
throw error;
}
Solution 4: Delete duplicate
# If truly duplicate, delete one
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "DELETE FROM \"User\" WHERE id = 'ID_TO_DELETE';"
Prevention
- Check before create - Query first to check existence
- Use upsert - Update or create atomically
- Unique indexes - Database enforces uniqueness
- Case normalization - Store emails lowercase
Foreign Key Constraint
Severity: 🟡 Medium
Symptoms
Error: Foreign key constraint failed on the field: `campaignId`
Or:
Error: An operation failed because it depends on one or more records that were required but not found. Record to update not found.
Common Causes
- Parent doesn't exist - Referenced record missing
- Wrong ID - Typo in foreign key value
- Delete order - Trying to delete parent before children
- Null constraint - Foreign key required but null provided
Solutions
Solution 1: Verify parent exists
# Check campaign exists
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
-c "SELECT id, name FROM \"Campaign\" WHERE id = 'CAMPAIGN_ID';"
Solution 2: Create parent first
// Create campaign first
const campaign = await prisma.campaign.create({
data: { name: 'My Campaign', ... }
});
// Then create email with campaignId
const email = await prisma.campaignEmail.create({
data: {
campaignId: campaign.id, // Use created campaign's ID
...
}
});
Solution 3: Delete children first
// Delete all emails in campaign
await prisma.campaignEmail.deleteMany({
where: { campaignId }
});
// Then delete campaign
await prisma.campaign.delete({
where: { id: campaignId }
});
// Or use cascade delete in schema:
// @@relation(onDelete: Cascade)
Solution 4: Use transactions
// Ensure atomicity
await prisma.$transaction([
prisma.campaignEmail.deleteMany({ where: { campaignId } }),
prisma.campaign.delete({ where: { id: campaignId } })
]);
Prevention
- Cascade deletes - Configure in schema where appropriate
- Soft deletes - Flag as deleted instead of removing
- Validation - Check foreign keys exist before creating
- Transactions - Use for multi-step operations
Frontend Errors
Network Error
Severity: 🟠 High
Symptoms
Browser console:
Error: Network Error
Or:
AxiosError: Request failed with status code undefined
User sees: API request fails, loading spinner never stops.
Common Causes
- API down - API container not running
- Wrong API URL - VITE_API_URL misconfigured
- CORS issue - Browser blocking request
- Network timeout - Request taking too long
Solutions
Solution 1: Check API status
# Is API running?
docker compose ps api
# Check API logs
docker compose logs api --tail=50
# Test API directly
curl http://localhost:4000/api/health
Solution 2: Verify API URL
# Check admin .env
cat admin/.env
# Should have:
VITE_API_URL=http://localhost:4000
# In Docker, use:
VITE_API_URL=http://api:4000
Solution 3: Check browser console
Press F12, check:
- Network tab - Does request appear? What's the status?
- Console tab - Any CORS errors?
Solution 4: Test from different client
# From command line
curl http://localhost:4000/api/campaigns
# If this works but browser doesn't, it's a CORS issue
Prevention
- Health checks - Monitor API availability
- Error boundaries - Catch and display network errors
- Retry logic - Auto-retry failed requests
- Offline detection - Detect and handle offline state
CORS Errors
Severity: 🟠 High
Symptoms
Browser console:
Access to XMLHttpRequest at 'http://localhost:4000/api/users' from origin
'http://localhost:3000' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested resource.
Common Causes
- Missing CORS config - API not configured for CORS
- Wrong origin - Admin URL not in allowed origins
- Credentials flag - withCredentials set but not allowed
- Preflight failure - OPTIONS request failing
Solutions
Solution 1: Check API CORS configuration
In api/src/server.ts:
app.use(cors({
origin: process.env.CORS_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true
}));
Solution 2: Verify CORS_ORIGINS
# Check .env
cat .env | grep CORS_ORIGINS
# Should include admin URL:
CORS_ORIGINS=http://localhost:3000,https://app.cmlite.org
Solution 3: Add origin temporarily
For development:
# In .env
CORS_ORIGINS=* # Allow all origins (dev only!)
# Restart API
docker compose restart api
Solution 4: Check preflight request
In browser Network tab:
- Find OPTIONS request before actual request
- Check if it returns 200 OK
- Check response headers include:
- Access-Control-Allow-Origin
- Access-Control-Allow-Methods
- Access-Control-Allow-Headers
Prevention
- Explicit origins - List all allowed origins
- Environment-based - Different origins per environment
- Credentials support - Enable if using cookies/auth
- Preflight caching - Cache OPTIONS responses
!!! warning "Security Note"
Never use CORS_ORIGINS=* in production with credentials enabled.
Module Not Found
Severity: 🟡 Medium
Symptoms
Error: Cannot find module '@/components/MyComponent'
Or:
Module not found: Can't resolve 'some-package'
Common Causes
- Missing dependency - Package not installed
- Wrong import path - Typo in path
- Path alias issue - @ alias not configured
- Case sensitivity - Wrong case in filename
Solutions
Solution 1: Install missing package
cd admin
# Install package
npm install some-package
# Or if dev dependency
npm install -D some-package
# Restart dev server
npm run dev
Solution 2: Check import path
// Wrong:
import MyComponent from '@/Component/MyComponent';
// Right:
import MyComponent from '@/components/MyComponent';
// Verify file exists:
// admin/src/components/MyComponent.tsx
Solution 3: Verify path alias
In admin/vite.config.ts:
export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
});
In admin/tsconfig.json:
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
}
}
Solution 4: Clear cache and reinstall
cd admin
# Remove node_modules and lock file
rm -rf node_modules package-lock.json
# Reinstall
npm install
# Restart
npm run dev
Prevention
- Type checking - Use TypeScript for import validation
- IDE support - Configure path aliases in IDE
- Linting - Use ESLint with import plugin
- Documentation - Document custom path aliases
Hydration Errors
Severity: 🟡 Medium
Symptoms
Browser console:
Warning: Text content did not match. Server: "..." Client: "..."
Or:
Error: Hydration failed because the initial UI does not match what was
rendered on the server.
Common Causes
- Date formatting - Server/client timezone difference
- Random values - Using Math.random() or uuid
- localStorage - Reading from localStorage during render
- User agent - Checking window.navigator during SSR
- Third-party scripts - Injected by browser extensions
Solutions
Solution 1: Use useEffect for client-only code
// Wrong:
const Component = () => {
const value = localStorage.getItem('key');
return <div>{value}</div>;
};
// Right:
const Component = () => {
const [value, setValue] = useState<string | null>(null);
useEffect(() => {
setValue(localStorage.getItem('key'));
}, []);
return <div>{value}</div>;
};
Solution 2: Consistent date formatting
// Wrong:
<div>{new Date().toLocaleString()}</div> // Varies by locale
// Right:
import dayjs from 'dayjs';
<div>{dayjs().format('YYYY-MM-DD HH:mm:ss')}</div>
Solution 3: suppressHydrationWarning for known mismatches
// For values that intentionally differ (like timestamps)
<time suppressHydrationWarning>
{new Date().toISOString()}
</time>
Solution 4: Check browser extensions
Disable browser extensions temporarily to see if error persists.
Prevention
- Avoid client-only APIs during render - Use useEffect
- Consistent formatting - Same format server and client
- Test without extensions - Regular testing
- React DevTools - Use to identify mismatches
!!! note "Changemaker Lite V2" Current admin is CSR (Client-Side Rendered) only, so hydration errors shouldn't occur. This section is for future SSR/SSG implementations.
File Upload Errors
File Too Large
Severity: 🟡 Medium
Symptoms
{
"error": "Payload Too Large",
"message": "File size exceeds maximum of 10485760 bytes"
}
Or browser:
Request Entity Too Large
Common Causes
- File exceeds limit - Video larger than 10GB
- Nginx limit - Reverse proxy blocking
- Wrong content type - Not multipart/form-data
- Network timeout - Upload taking too long
Solutions
Solution 1: Check file size
// Before upload
const file = event.target.files[0];
const maxSize = 10 * 1024 * 1024 * 1024; // 10GB
if (file.size > maxSize) {
alert(`File too large. Max size: ${maxSize / 1024 / 1024 / 1024}GB`);
return;
}
Solution 2: Increase limits
In api/src/modules/media/routes/upload.routes.ts:
fastify.register(multipart, {
limits: {
fileSize: 10 * 1024 * 1024 * 1024 // 10GB
}
});
In nginx/conf.d/api.conf:
client_max_body_size 10G;
Solution 3: Use chunked upload
For very large files, implement resumable upload:
// TODO: Implement chunked upload in Phase 15
Solution 4: Compress video
# Before uploading, compress with ffmpeg
ffmpeg -i input.mp4 -c:v libx264 -crf 23 -c:a aac output.mp4
Prevention
- Client validation - Check size before upload
- Progress indicator - Show upload progress
- Compression - Compress large videos
- Chunked uploads - For files > 1GB
Invalid File Type
Severity: 🟢 Low
Symptoms
{
"error": "Bad Request",
"message": "Invalid file type. Allowed: mp4, mov, avi, mkv, webm, m4v, flv"
}
Common Causes
- Wrong extension - File has unsupported extension
- Missing extension - Filename has no extension
- Mismatched extension - Extension doesn't match content
- MIME type issue - Browser sends wrong MIME type
Solutions
Solution 1: Check supported formats
Supported video formats:
- MP4 (.mp4)
- MOV (.mov)
- AVI (.avi)
- MKV (.mkv)
- WebM (.webm)
- M4V (.m4v)
- FLV (.flv)
Solution 2: Convert video
# Convert to MP4 (most compatible)
ffmpeg -i input.avi -c:v libx264 -c:a aac output.mp4
Solution 3: Check file extension
const file = event.target.files[0];
const ext = file.name.split('.').pop().toLowerCase();
const allowed = ['mp4', 'mov', 'avi', 'mkv', 'webm', 'm4v', 'flv'];
if (!allowed.includes(ext)) {
alert(`Invalid file type: .${ext}`);
return;
}
Solution 4: Verify with file command
# Check actual file type
file video.mp4
# Should show:
# video.mp4: ISO Media, MP4 v2 [ISO 14496-14]
Prevention
- Client validation - Check extension before upload
- MIME type checking - Validate content type
- File magic numbers - Check file signature
- Clear documentation - List supported formats
Upload Timeout
Severity: 🟡 Medium
Symptoms
Error: timeout of 30000ms exceeded
Or:
504 Gateway Timeout
Common Causes
- Slow network - Large file, slow connection
- Server timeout - Request timeout too short
- Processing delay - FFprobe taking too long
- Network interruption - Connection dropped
Solutions
Solution 1: Increase timeout
In admin/src/lib/media-api.ts:
export const mediaApi = axios.create({
baseURL: import.meta.env.VITE_MEDIA_API_URL,
timeout: 300000 // 5 minutes instead of 30 seconds
});
Solution 2: Check upload progress
await mediaApi.post('/upload', formData, {
onUploadProgress: (progressEvent) => {
const percent = (progressEvent.loaded / progressEvent.total) * 100;
console.log(`Upload: ${percent.toFixed(2)}%`);
}
});
Solution 3: Increase nginx timeout
In nginx/conf.d/api.conf:
proxy_read_timeout 300s;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
Solution 4: Upload via chunks
// TODO: Implement chunked upload for large files
Prevention
- Progress indicator - Show upload progress
- Generous timeouts - Allow enough time for large files
- Retry logic - Auto-retry on network errors
- Chunked uploads - For files > 1GB
Email Errors
SMTP Connection Failed
Severity: 🔴 Critical
Symptoms
API logs:
Error: Connection timeout
Error: connect ECONNREFUSED 127.0.0.1:587
Or:
Error: Invalid login: 535-5.7.8 Username and Password not accepted
Common Causes
- SMTP server down - Mail server unreachable
- Wrong credentials - Invalid username/password
- Port blocked - Firewall blocking SMTP port
- TLS/SSL issue - Certificate validation failed
Solutions
Solution 1: Test SMTP connection
# Test with telnet
telnet smtp.gmail.com 587
# Should connect and show:
# 220 smtp.gmail.com ESMTP...
Solution 2: Verify SMTP configuration
# Check .env
cat .env | grep SMTP
# Required settings:
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
SMTP_FROM=your-email@gmail.com
Solution 3: Use test mode
# In .env
EMAIL_TEST_MODE=true
# Restart API
docker compose restart api
# Emails now sent to MailHog (http://localhost:8025)
Solution 4: Check Gmail app password
For Gmail:
- Enable 2-factor authentication
- Generate app password at https://myaccount.google.com/apppasswords
- Use app password (not regular password) in SMTP_PASS
Solution 5: Test with curl
# Send test email via API
curl -X POST http://localhost:4000/api/test-email \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"to": "test@example.com",
"subject": "Test Email",
"text": "This is a test"
}'
Prevention
- Test mode for dev - Use MailHog locally
- Monitor SMTP health - Alert on connection failures
- Fallback providers - Configure backup SMTP server
- Queue system - BullMQ retries failed emails
Template Not Found
Severity: 🟠 High
Symptoms
API logs:
Error: Email template not found: campaign-email
Or:
Error: ENOENT: no such file or directory, open 'templates/campaign-email.html'
Common Causes
- Missing template file - Template not created
- Wrong template name - Typo in template name
- Wrong path - Looking in wrong directory
- Deleted template - Template was removed
Solutions
Solution 1: Check template exists
# List all templates
docker compose exec api ls -la templates/
# Should show:
# campaign-email.html
# shift-confirmation.html
# verification-email.html
# etc.
Solution 2: Verify template name
In api/src/services/email.service.ts:
// Template names must match filenames (without .html)
await emailService.sendEmail({
to: email,
subject: 'Campaign Email',
template: 'campaign-email', // Looks for templates/campaign-email.html
variables: { ... }
});
Solution 3: Create missing template
# Create template
docker compose exec api sh -c 'cat > templates/my-template.html << EOF
<!DOCTYPE html>
<html>
<body>
<h1>Hello {{name}}</h1>
<p>{{message}}</p>
</body>
</html>
EOF'
Solution 4: Use email template system
# Navigate to admin UI
http://localhost:3000/app/email-templates
# Create template there (saved to database + file)
Prevention
- Seed templates - Include in database seed
- Template management - Use admin UI to manage
- Version control - Keep templates in git
- Validation - Check template exists before sending
Variable Missing
Severity: 🟡 Medium
Symptoms
Email received with placeholders not replaced:
Hello {{name}},
Your campaign {{campaignName}} is ready.
Or API logs:
Warning: Template variable 'campaignName' not provided
Common Causes
- Variable not passed - Missing from variables object
- Variable name mismatch - Typo in variable name
- Wrong template - Using wrong template
- Case sensitivity - Variable name case mismatch
Solutions
Solution 1: Check template variables
In template file:
<!-- templates/campaign-email.html -->
<h1>Hello {{firstName}}</h1>
<p>Your campaign "{{campaignName}}" is ready.</p>
<p>Visit: {{campaignUrl}}</p>
Solution 2: Provide all variables
await emailService.sendEmail({
to: email,
subject: 'Campaign Ready',
template: 'campaign-email',
variables: {
firstName: user.name.split(' ')[0],
campaignName: campaign.name,
campaignUrl: `${process.env.PUBLIC_URL}/campaigns/${campaign.id}`
}
});
Solution 3: Use default values
<!-- In template, provide fallback -->
<h1>Hello {{firstName || 'Friend'}}</h1>
Solution 4: Validate before sending
// Check all required variables exist
const required = ['firstName', 'campaignName', 'campaignUrl'];
const missing = required.filter(key => !variables[key]);
if (missing.length > 0) {
throw new Error(`Missing template variables: ${missing.join(', ')}`);
}
Prevention
- Template validation - Check variables on save
- TypeScript types - Type template variables
- Documentation - Document required variables
- Default values - Provide sensible defaults
Quick Reference Table
| Error Code/Message | Category | Common Cause | Quick Fix | Severity |
|---|---|---|---|---|
| 401 Unauthorized | Auth | Token expired | Re-login | 🟠 |
| 403 Forbidden | Auth | Wrong role | Check user role | 🟠 |
| 404 Not Found | API | Wrong URL/ID | Verify resource exists | 🟢 |
| 422 Unprocessable | Validation | Constraint violation | Check validation details | 🟡 |
| 500 Server Error | API | Code bug | Check API logs | 🔴 |
| ECONNREFUSED | Database | DB not running | Start database | 🔴 |
| Too many connections | Database | Connection leak | Restart API | 🟠 |
| Unique constraint | Database | Duplicate record | Use upsert or different value | 🟡 |
| Foreign key constraint | Database | Parent missing | Create parent first | 🟡 |
| Network Error | Frontend | API down | Check API status | 🟠 |
| CORS Error | Frontend | Origin not allowed | Add to CORS_ORIGINS | 🟠 |
| Module not found | Frontend | Missing package | npm install | 🟡 |
| File too large | Upload | Exceeds 10GB | Compress or increase limit | 🟡 |
| Invalid file type | Upload | Wrong format | Convert to MP4 | 🟢 |
| Upload timeout | Upload | Slow network | Increase timeout | 🟡 |
| SMTP failed | Wrong credentials | Check SMTP config | 🔴 | |
| Template not found | Missing file | Create template | 🟠 | |
| Variable missing | Not provided | Add to variables object | 🟡 |
When to Report Bugs
Report These
✅ Unexpected behavior - System does something wrong
- 500 errors (unless caused by your config)
- Data corruption
- Security vulnerabilities
- Performance regressions
✅ Missing features - Documented feature doesn't work
- API endpoint returns 404 but is documented
- UI button does nothing
- Feature flag doesn't enable feature
✅ Unclear documentation - Can't figure out how to do something
- Documentation contradicts actual behavior
- Missing setup steps
- Confusing error messages
Don't Report These
❌ Configuration errors - Your setup is wrong
- Missing .env variables
- Wrong database credentials
- Port conflicts
❌ Environment issues - Your system is incompatible
- Old Docker version
- Missing dependencies
- Network restrictions
❌ User errors - Misunderstanding how to use
- Wrong API endpoint used
- Invalid data format
- Permission errors from lack of role
How to Report
- Check this troubleshooting guide first
- Search existing GitHub issues
- If new, create issue with:
- Clear title describing problem
- Steps to reproduce
- Expected vs actual behavior
- Relevant logs (sanitize sensitive data)
- System information (Docker version, OS, etc.)
Related Documentation
General Documentation
- Installation Guide - Setup instructions
- Architecture Overview - System design
- API Reference - API endpoints
Specific Troubleshooting
- Docker Issues - Container problems
- Database Issues - PostgreSQL errors
- Auth Issues - Authentication problems
- Geocoding Issues - Map and geocoding
- Email Issues - SMTP and templates
- Monitoring Issues - Prometheus and Grafana
Support
- FAQ - Frequently asked questions
- Performance Optimization - Speed improvements
- GitHub Issues - Report bugs
Last Updated: February 2026 Version: V2.0 Status: Complete