Foundational conventions and patterns that every developer must follow. These are not features — they are engineering standards that ensure the codebase stays maintainable, debuggable, and resilient under load.
# Server
NODE_ENV=development
PORT=5000
# Database
DB_HOST=localhost
DB_PORT=3306
DB_NAME=dispatch_core
DB_USER=root
DB_PASS=
# WebSocket
WS_CORS_ORIGIN=http://localhost:5173
# Frontend
FRONTEND_URL=https://dispatchcoredelivery.vercel.app
# Map Tiles
MAPTILER_API_KEY=
# JWT
JWT_ACCESS_SECRET=change-me-access-secret-at-least-32-chars
JWT_REFRESH_SECRET=change-me-refresh-secret-at-least-32-chars
# Firebase
FIREBASE_SERVICE_ACCOUNT_PATH=./firebase-service-account.json
FIREBASE_DATABASE_URL=https://your-project-id.firebaseio.com
# Email (Nodemailer SMTP)
# For Render/production, use port 587 with secure=false (STARTTLS)
# For Gmail with app password, port 465 with secure=true also works
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your_email@gmail.com
SMTP_PASS=your_app_password
EMAIL_FROM="dispatchCore <noreply@dispatchcore.com>"Validate all required env vars on server boot using Joi. If any are missing, crash immediately with a clear message.
// config/env.js
const Joi = require('joi');
const schema = Joi.object({
NODE_ENV: Joi.string().valid('development', 'production', 'test').required(),
PORT: Joi.number().default(5000),
DB_HOST: Joi.string().required(),
DB_NAME: Joi.string().required(),
// ... all required vars
}).unknown();
const { error, value } = schema.validate(process.env);
if (error) {
console.error('Config validation error:', error.message);
process.exit(1);
}Every API response must follow this shape:
// Success
{
"success": true,
"data": { /* payload */ },
"meta": { "page": 1, "total": 50 } // optional
}
// Error
{
"success": false,
"error": {
"code": "ORDER_ALREADY_ASSIGNED",
"message": "This order has already been assigned to another driver.",
"status": 409
}
}// utils/response.js
exports.success = (res, data, meta = null, status = 200) => {
return res.status(status).json({ success: true, data, meta });
};
exports.error = (res, code, message, status = 400) => {
return res.status(status).json({
success: false,
error: { code, message, status }
});
};One global error middleware catches all unhandled errors:
// middlewares/errorHandler.js
module.exports = (err, req, res, next) => {
const status = err.status || 500;
const code = err.code || 'INTERNAL_ERROR';
const message = err.message || 'Something went wrong';
if (process.env.NODE_ENV === 'development') {
console.error(err.stack);
}
return res.status(status).json({
success: false,
error: { code, message, status }
});
};// utils/errors.js
class AppError extends Error {
constructor(message, status, code) {
super(message);
this.status = status;
this.code = code;
}
}
class ConflictError extends AppError {
constructor(message = 'Resource conflict') {
super(message, 409, 'CONFLICT');
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404, 'NOT_FOUND');
}
}All request bodies are validated before reaching the service layer. Six validator files cover all endpoints:
| Validator | Endpoints Covered |
|---|---|
authValidator.js |
Login |
orderValidator.js |
Create order, list order, assign order, get order |
bidValidator.js |
Place bid |
driverValidator.js |
Create driver, signup, verify, update, password, vehicle, register route, find nearby |
locationValidator.js |
GPS ping, public tracking |
superadminValidator.js |
Update settings |
// validators/orderValidator.js
const { body, param } = require('express-validator');
exports.createOrder = [
body('pickup_lat').isFloat({ min: -90, max: 90 }),
body('pickup_lng').isFloat({ min: -180, max: 180 }),
body('delivery_lat').isFloat({ min: -90, max: 90 }),
body('delivery_lng').isFloat({ min: -180, max: 180 }),
body('priority').isIn(['LOW', 'NORMAL', 'HIGH', 'URGENT']),
body('weight_kg').optional().isFloat({ min: 0 }),
];// middlewares/validate.js
const { validationResult } = require('express-validator');
module.exports = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid input',
details: errors.array(),
status: 400
}
});
}
next();
};Use Winston with consistent log levels and request context:
// config/logger.js
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
})
]
});| Level | When to Use |
|---|---|
error |
Failed transactions, DB connection loss, unhandled exceptions |
warn |
Lock timeouts, bid on already-assigned order, rate limit hit |
info |
Assignment created, bid accepted, driver went online, WebSocket connect/disconnect |
debug |
GPS ping received, WebSocket room join, query execution time |
Never use sequelize.sync(). Use Sequelize CLI migrations:
npx sequelize-cli migration:generate --name create-orders
npx sequelize-cli db:migrate
npx sequelize-cli db:migrate:undo # rollback// config/database.js (via models/index.js)
pool: {
min: 2,
max: 10,
acquire: 30000, // ms to wait for connection
idle: 10000 // ms before releasing idle connection
}| Entity | Convention | Example |
|---|---|---|
| Table names | snake_case, plural | driver_location_logs |
| Column names | snake_case | company_id, created_at |
| Foreign keys | {referenced_table_singular}_id |
driver_id, order_id |
| Indexes | idx_{table}_{columns} |
idx_orders_company_status |
| Model global settings | timestamps: true, underscored: true |
Auto created_at, updated_at |
// config/cors.js
const corsOptions = {
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization', 'x-company-id', 'x-driver-id'],
};Authorization is required for bearer-token fallback. Legacy x-company-id and x-driver-id are still accepted for backward compatibility on non-authenticated flows.
Helmet.js is applied globally for XSS, clickjacking, and MIME-sniff protection.
// middlewares/rateLimiter.js
// Standard API: 100 requests per 15 minutes
exports.apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
});
// GPS pings: 20 per minute (1 every 3 seconds)
exports.locationLimiter = rateLimit({
windowMs: 60 * 1000,
max: 20,
});All company-scoped endpoints pass through tenantResolver middleware:
// middlewares/tenantResolver.js
// Resolves company context from req.identity (normalized identity)
// req.identity is populated by requestIdentity middleware, prioritizing trusted JWT claims
// over client-supplied headers.
// Sets req.tenantId for downstream use.
// All Order/Assignment/Company queries filter by req.tenantId.Authentication is JWT-based with cookie-first transport and bearer fallback:
| Source | Purpose | Notes |
|---|---|---|
accessToken (HttpOnly cookie) |
Primary authenticated identity | Preferred in both local and production environments |
Authorization: Bearer <token> |
Fallback identity transport | Used when third-party cookies are blocked |
x-company-id / x-driver-id |
Legacy identity hints | Used only when trusted auth context is absent |
// lib/api.ts
// Wraps fetch() with automatic:
// - Base URL injection (VITE_API_URL)
// - JSON content-type headers
// - credentials: 'include' for auth cookies
// - Authorization bearer fallback from stored access token
// - Response unwrapping (extracts data from { success, data, meta } envelope)
// - Error handlingServices are role-scoped with TypeScript interfaces:
// services/driver/dashboard.ts
export async function fetchDriverStats(): Promise<DriverStats> { ... }
export async function fetchActiveDeliveries(): Promise<ActiveDelivery[]> { ... }
export async function fetchCompletedDeliveries(): Promise<ActiveDelivery[]> { ... }
export async function fetchEarningsChart(): Promise<EarningsChart[]> { ... }Custom hooks compose services into reactive state:
// hooks/driver/useDashboard.ts
export function useDriverStats() { ... }
export function useActiveDeliveries() { ... }
export function useCompletedDeliveries() { ... }Driver dashboards poll for data updates every 30 seconds to stay in sync with dispatcher-side changes:
useEffect(() => {
// Initial load
load();
// Auto-refresh every 30s
const interval = setInterval(() => refresh(), 30_000);
return () => clearInterval(interval);
}, []);GET /api/health → { status: 'healthy', uptime, database: 'connected', timestamp }
// server.js
const shutdown = async (signal) => {
// 1. Stop accepting new HTTP connections
// 2. Close WebSocket connections
// 3. Close database pool
// 4. Exit
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));All outgoing emails go through services/mailService.js for consistent logging, error handling, and template rendering.
Located in utils/emailTemplates/:
baseLayout.js— Shared HTML wrapper with dark theme stylingverificationTemplate.js— Email verification for new userswelcomeTemplate.js— Welcome email after registrationorderAssignedTemplate.js— Order assignment notification to driversorderBidTemplate.js— Bid status updates (accepted/rejected/pending)orderTrackingTemplate.js— Tracking link sent to recipients
// config/mail.js
const transporter = nodemailer.createTransport({
host: env.smtp.host,
port: env.smtp.port,
secure: env.smtp.secure,
auth: { user: env.smtp.user, pass: env.smtp.pass },
family: 4, // Force IPv4 (required for Render)
});| Event | Email Sent | Recipient |
|---|---|---|
| New user registration | Welcome email | User |
| Company created | Welcome + verification email | Company |
| Order assigned | Assignment notification | Driver |
| Bid accepted | Bid accepted email | Winning driver |
| Bid rejected | Bid rejected email | Driver |
| Order picked up | Tracking link | Recipient |
Emails are sent asynchronously with error handling that doesn't block the main request:
mailService.sendWelcomeEmail(user, 'driver').catch((err) => {
logger.error({ err, userId }, 'Failed to send welcome email');
});Firebase RTDB replaces Socket.io for real-time updates. All live events flow through Firebase:
- Driver location updates
- Order status changes
- Assignment notifications
- Marketplace bid updates
Initialized on server startup with service account credentials:
// config/firebase.js
const admin = require('firebase-admin');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: env.firebase.databaseUrl,
});Events are written to Firebase paths like:
/drivers/{driverId}/location— GPS coordinates/orders/{orderId}/status— Order status updates/companies/{companyId}/assignments— Assignment notifications
Single socket handler file (sockets/index.js) manages all room joins:
| Event | Room Created |
|---|---|
join:company |
company:{id}:dispatchers |
join:marketplace |
company:{id}:marketplace |
join:driver |
driver:{id} |
join:tracking |
order:{id}:tracking |
join:messages |
order:{id}:chat:{channel} |
High-frequency GPS pings are sent directly over WebSocket (location:ping event) as an alternative to the REST endpoint, reducing HTTP overhead.
- Client auto-reconnects with exponential backoff
- Server detects stale sockets via
pingTimeout(20 seconds) - On reconnect, client re-joins all previous rooms
- Tailwind CSS v4 — utility-first CSS
- CSS Custom Properties — theme tokens (light/dark/system via
useThemehook; default:system) - No inline styles — all styling through Tailwind classes
- Settings pages — use the shared settings layout and theme system. Current settings screens may still use some role-specific accent colors while the visual language is being standardized.
- Dashboard pages — may use role-specific accent colors for role branding
- Framer Motion — page transitions via
<PageTransition>wrapper - AnimatePresence — exit animations on route changes
- CSS transitions — hover effects, micro-interactions
- All monetary values displayed in Indian Rupees (₹) via
formatINR()inlib/currency.ts - Distances calculated via Haversine formula in
lib/geo.ts
- Sidebars are role-specific:
sidebar.tsx,driver-sidebar.tsx,employed-driver-sidebar.tsx,superadmin-sidebar.tsx - Theme toggle has been removed from sidebars; only
DriverSidebaraccepts an optionaluserNameprop; the other three accept no props - Pages are self-contained with their own data fetching via
useEffect - Loading states use
<LoadingPackage />animated component - Empty states use
<EmptyState />component with icons
main ← production-ready code
└── develop ← integration branch
├── feature/order-crud
├── feature/bidding-engine
├── feature/websocket-rooms
└── fix/assignment-race-condition
feat: add order creation endpoint
fix: prevent double assignment with serializable lock
docs: update API reference with bid endpoints
refactor: extract validation into middleware
Committed to repo (no secrets). Every new developer copies it:
cp .env.example .env
# Then fill in local values