New Page (will be separated)
Table of Contents
- System Overview
- Architecture Layers
- Database Architecture
- Authentication & Authorization
- API Design
- WebSocket Architecture
- File Management
- Scheduled Tasks
- Security Architecture
- Scalability Considerations
System Overview
ON Internal API is a multi-tenant, microservices-style backend system built on Node.js and Express.js. It manages multiple business domains through a unified API gateway while maintaining separation of concerns through modular architecture.
Design Principles
- Modularity: Each business domain (employee, asset, fleet, etc.) is isolated in its own module
- Separation of Concerns: Clear separation between routes, controllers, models, and utilities
- Database Segregation: Different databases for different domains to ensure data isolation
- Security First: Multiple layers of security (Helmet, CORS, JWT, validation)
- Real-time Capable: WebSocket support for live updates
- Stateless: JWT-based authentication allows horizontal scaling
Architecture Layers
1. Entry Point Layer (server.js)
┌─────────────────────────────────────────────────────┐
│ server.js │
├─────────────────────────────────────────────────────┤
│ - Load environment variables (dotenv) │
│ - Initialize Express application │
│ - Configure security middleware (Helmet, CORS) │
│ - Setup logging (Morgan with colored output) │
│ - Connect to all databases (Sequelize/Mongoose) │
│ - Create directory structure │
│ - Load all routes │
│ - Initialize WebSocket server │
│ - Start HTTP server │
│ - Load scheduled tasks │
└─────────────────────────────────────────────────────┘
Key Responsibilities:
- Application bootstrap
- Global configuration
- Middleware stack setup
- Database connection management
- Route registration
- Server initialization
2. Middleware Layer
┌────────────────────────────────────────────────────┐
│ Middleware Pipeline │
├────────────────────────────────────────────────────┤
│ 1. CORS (Cross-Origin Resource Sharing) │
│ 2. Helmet (Security headers) │
│ 3. Morgan (Request logging with colors) │
│ 4. Express JSON/URL-encoded parsers (10MB limit) │
│ 5. Custom middleware (per route): │
│ - authJwt.verifyToken (JWT validation) │
│ - contentTypeValid (Content-Type check) │
│ - validator.validate (Schema validation) │
│ - checkEmployee/checkFleet (Role validation) │
│ 6. Error handler (errorParamHandler) │
└────────────────────────────────────────────────────┘
Middleware Components:
Security Middleware (app/middleware/authJwt.js):
verifyToken(req, res, next) {
// Extract token from header
// Verify JWT signature
// Decode and attach user to request
// Continue or reject
}
Validation Middleware (app/schemas/):
- Uses AJV for JSON Schema validation
- Validates request body, query params, and URL params
- Returns structured error messages
Error Handler (app/middleware/errorParamHandler.js):
- Catches all errors
- Formats error responses
- Logs errors
- Returns consistent JSON error format
3. Route Layer
Routes define API endpoints and map them to controllers:
// Example: app/routes/auth.routes.js
module.exports = function (app) {
app.post(
"/api/auth/login",
[
contentTypeValid("application/json"),
validator.validate({ body: auth.login }),
],
authController.login
);
app.get(
"/api/auth/account",
authJwt.verifyToken,
authController.loadAccount
);
};
Route Organization:
- One route file per module
- Middleware applied declaratively
- Clear HTTP method usage
- RESTful design patterns
4. Controller Layer
Controllers contain business logic:
┌─────────────────────────────────────────────────────┐
│ Controller Responsibilities │
├─────────────────────────────────────────────────────┤
│ 1. Extract data from request (body, query, params) │
│ 2. Call database operations via models │
│ 3. Process business logic │
│ 4. Format response data │
│ 5. Handle errors │
│ 6. Return HTTP response │
└─────────────────────────────────────────────────────┘
Controller Pattern:
exports.methodName = async (req, res) => {
try {
// 1. Extract data
const { param } = req.body;
// 2. Validate business rules
if (!param) throw new Error("Missing parameter");
// 3. Database operations
const result = await Model.findOne({ where: { id } });
// 4. Process data
const processed = processData(result);
// 5. Return response
res.status(200).json({
success: true,
data: processed
});
} catch (error) {
// 6. Error handling
res.status(400).json({
success: false,
message: error.message
});
}
};
5. Model Layer
Models define database schemas using Sequelize (PostgreSQL) or Mongoose (MongoDB):
// Example Sequelize model
module.exports = (sequelize, Sequelize) => {
const Model = sequelize.define("table_name", {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
name: {
type: Sequelize.STRING,
allowNull: false
},
// ... other fields
}, {
timestamps: true,
tableName: "table_name"
});
return Model;
};
Database Architecture
Multi-Database Strategy
The system uses four separate databases:
┌────────────────────────────────────────────────────┐
│ Database Layer │
├────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Sunshine DB │ │ Fleet DB │ │
│ │ (PostgreSQL) │ │ (PostgreSQL) │ │
│ ├─────────────────┤ ├─────────────────┤ │
│ │ - Users │ │ - Drivers │ │
│ │ - Employees │ │ - Vehicles │ │
│ │ - Attendance │ │ - Routes │ │
│ │ - Leave/Permit │ │ - Tracking │ │
│ │ - Assets │ │ │ │
│ │ - Ticketing │ │ │ │
│ │ - Shifts │ │ │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ CMS DB │ │ Mitra DB │ │
│ │ (PostgreSQL) │ │ (PostgreSQL + │ │
│ ├─────────────────┤ │ MongoDB) │ │
│ │ - Blog Posts │ ├─────────────────┤ │
│ │ - Ads │ │ - Mitra data │ │
│ │ - Training │ │ - Documents │ │
│ │ - Career │ │ │ │
│ │ - Departments │ │ │ │
│ │ - Drop Points │ │ │ │
│ └─────────────────┘ └─────────────────┘ │
└────────────────────────────────────────────────────┘
Database Configuration
Connection Pooling (for performance):
pool: {
max: 5, // Maximum connections
min: 0, // Minimum connections
acquire: 30000, // Max time to get connection (ms)
idle: 10000 // Max idle time before closing (ms)
}
Database Selection Pattern
// In models/index.js
const sequelizeSunshine = new Sequelize(sunshineDB.DB, ...);
const sequelizeFleet = new Sequelize(fleetDB.DB, ...);
const sequelizeCms = new Sequelize(cmsDB.DB, ...);
// Models are registered to specific connections
db.employee = require("./employee.model")(sequelizeSunshine, Sequelize);
db.driver = require("./driver.model")(sequelizeFleet, Sequelize);
Data Relationships
Cross-Database Relationships:
- Avoided when possible for better independence
- When needed, handled at application layer (not DB constraints)
- Use foreign keys within same database
Example:
// Employee (Sunshine DB) -> Asset Assignment (Sunshine DB) ✓
// Employee has Assets relationship
// Employee (Sunshine DB) -> Driver (Fleet DB) ✗
// Handled via employee_id field and application logic
Authentication & Authorization
JWT Authentication Flow
┌─────────┐ ┌─────────┐ ┌──────────┐
│ Client │ │ API │ │ Database │
└────┬────┘ └────┬────┘ └─────┬────┘
│ │ │
│ POST /api/auth/login │
│ { username, password } │
├──────────────────────> │
│ │ │
│ │ Query user │
│ ├──────────────────────>
│ │ │
│ │ User data │
│ <──────────────────────┤
│ │ │
│ │ Verify password │
│ │ (bcrypt.compare) │
│ │ │
│ │ Generate JWT │
│ │ (sign with secret) │
│ │ │
│ { token, user } │ │
<──────────────────────┤ │
│ │ │
│ │ │
│ GET /api/protected │ │
│ Header: x-access-token: <JWT> │
├──────────────────────> │
│ │ │
│ │ Verify JWT │
│ │ (verify signature) │
│ │ │
│ │ Decode payload │
│ │ Attach to req.user │
│ │ │
│ │ Process request │
│ ├──────────────────────>
│ │ │
│ Response data │ │
<──────────────────────┤ │
│ │ │
Authentication Components
Login Process (app/controllers/auth.controller.js):
- Receive username/password
- Query database for user
- Verify password with bcrypt
- Generate JWT token with user data
- Return token and user info
Token Verification (app/middleware/authJwt.js):
- Extract token from header (
x-access-token) - Verify token signature with JWT_SECRET
- Decode payload (user ID, roles, etc.)
- Attach decoded data to
req.user - Continue to next middleware/controller
Token Structure:
{
id: userId,
username: "user@example.com",
role: "admin",
iat: 1234567890, // Issued at
exp: 1234654290 // Expires at
}
Authorization Patterns
Role-Based Access Control:
// In middleware
if (req.user.role !== 'admin') {
return res.status(403).json({
message: "Unauthorized access"
});
}
Resource-Based Authorization:
// Verify user owns resource
const resource = await Model.findOne({
where: { id: resourceId, userId: req.user.id }
});
if (!resource) {
return res.status(403).json({
message: "Access denied"
});
}
API Design
RESTful Principles
Resource Naming:
- Plural nouns:
/api/employees,/api/assets - Nested resources:
/api/employees/:id/shifts - Actions as sub-resources:
/api/tickets/:id/assign
HTTP Methods:
-
GET: Read/retrieve data -
POST: Create new resource -
PUT: Update entire resource -
PATCH: Partial update -
DELETE: Remove resource
Status Codes:
-
200: Success -
201: Created -
400: Bad request (validation error) -
401: Unauthorized (not authenticated) -
403: Forbidden (not authorized) -
404: Not found -
500: Internal server error
Response Format
Success Response:
{
"success": true,
"data": {
"id": 1,
"name": "Example"
},
"message": "Operation successful"
}
Error Response:
{
"success": false,
"message": "Validation failed",
"errors": [
{
"field": "email",
"message": "Invalid email format"
}
]
}
Pagination (when applicable):
{
"success": true,
"data": [...],
"pagination": {
"page": 1,
"perPage": 20,
"total": 150,
"totalPages": 8
}
}
Input Validation
Using AJV JSON Schema:
// app/schemas/example.schema.js
module.exports = {
create: {
type: "object",
properties: {
name: { type: "string", minLength: 1 },
email: { type: "string", format: "email" },
age: { type: "integer", minimum: 18 }
},
required: ["name", "email"],
additionalProperties: false
}
};
Applied in routes:
app.post(
"/api/example",
validator.validate({ body: exampleSchema.create }),
controller.create
);
WebSocket Architecture
WebSocket Server Setup
// app/routes/ws.routes.js
const WebSocket = require("ws");
module.exports = (server, app) => {
const wss = new WebSocket.Server({ noServer: true });
server.on("upgrade", (request, socket, head) => {
// Parse URL to route to correct WebSocket handler
const pathname = url.parse(request.url).pathname;
if (pathname === "/ws/branding-approval") {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit("connection", ws, request);
});
}
});
wss.on("connection", (ws, request) => {
// Handle connection
ws.on("message", (message) => {
// Handle message
});
ws.on("close", () => {
// Handle disconnect
});
});
};
WebSocket Endpoints
-
Branding Approval (
/ws/branding-approval)- Real-time approval status updates
- Mobile and web clients
- Notification delivery
-
Ticketing (
/ws/ticketing)- Ticket status updates
- Assignment notifications
- Real-time chat (if applicable)
Connection Management
Heartbeat (Ping/Pong):
// interval.js
setInterval(function ping() {
wss.clients.forEach(function each(ws) {
if (ws.isAlive === false) {
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
}, 30000); // Every 30 seconds
Client Tracking:
// Track connected clients
const clients = new Map();
wss.on("connection", (ws, request) => {
const userId = extractUserId(request);
clients.set(userId, ws);
ws.on("close", () => {
clients.delete(userId);
});
});
Broadcasting:
// Send to all clients
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(data));
}
});
// Send to specific user
const userWs = clients.get(userId);
if (userWs && userWs.readyState === WebSocket.OPEN) {
userWs.send(JSON.stringify(data));
}
File Management
Upload Flow
┌─────────┐ ┌─────────┐ ┌──────────┐
│ Client │ │ API │ │ Storage │
└────┬────┘ └────┬────┘ └─────┬────┘
│ │ │
│ POST /api/upload/temp │
│ Content-Type: multipart/form-data │
│ Body: file │
├──────────────────────> │
│ │ │
│ │ Multer processes │
│ │ file upload │
│ │ │
│ │ Save to disk │
│ ├──────────────────────>
│ │ │
│ │ File saved │
│ <──────────────────────┤
│ │ │
│ { url, name } │ │
<──────────────────────┤ │
│ │ │
Storage Structure
resources/
├── static/ # Permanent files
│ ├── employee/ # Employee photos, documents
│ ├── ticketing/ # Ticket attachments
│ ├── cms/
│ │ ├── ad/ # Advertisement images
│ │ ├── blog/ # Blog post images
│ │ ├── training/ # Training materials
│ │ └── department/ # Department logos
│ ├── branding-approval/ # Branding assets
│ └── onapps/
│ └── voucher/ # Voucher images
└── temp/ # Temporary files (auto-deleted)
File Upload Middleware
// app/middleware/uploadTemp.js
const multer = require("multer");
const path = require("path");
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, path.join(__remoteDir, __stageEnv, "temp"));
},
filename: (req, file, cb) => {
const uniqueName = `${Date.now()}-${file.originalname}`;
cb(null, uniqueName);
}
});
const upload = multer({
storage: storage,
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
fileFilter: (req, file, cb) => {
// Validate file type if needed
cb(null, true);
}
});
File Serving
Temporary Files:
- Served via
/api/temp/:filename - Automatically deleted after first download
- Used for reports, exports, etc.
Static Files:
- Served via
/api/static/:filename - Permanent storage
- Used for user uploads, images, etc.
Scheduled Tasks
Task Scheduler Architecture
// app/schedulers/file-temporary.scheduler.js
const cron = require("node-cron");
exports.cleanTempFile = () => {
// Run every hour
cron.schedule("0 * * * *", async () => {
const tempDir = path.join(__remoteDir, __stageEnv, "temp");
const files = fs.readdirSync(tempDir);
files.forEach(file => {
const filePath = path.join(tempDir, file);
const stats = fs.statSync(filePath);
const now = Date.now();
const age = now - stats.mtimeMs;
// Delete files older than 24 hours
if (age > 24 * 60 * 60 * 1000) {
fs.unlinkSync(filePath);
}
});
});
};
Scheduled Tasks
-
Temporary File Cleanup
- Schedule: Every hour
- Action: Delete temp files older than 24 hours
- Purpose: Prevent disk space issues
-
Leave Auto-Rejection
- Schedule: Daily
- Action: Auto-reject pending leave requests past deadline
- Purpose: Workflow automation
Adding New Scheduled Tasks
// 1. Create scheduler file
// app/schedulers/your-task.scheduler.js
const cron = require("node-cron");
exports.yourTask = () => {
cron.schedule("0 0 * * *", async () => { // Daily at midnight
// Your task logic
});
};
// 2. Load in server.js
const yourTaskScheduler = require("./app/schedulers/your-task.scheduler");
yourTaskScheduler.yourTask();
Security Architecture
Security Layers
┌─────────────────────────────────────────────────────┐
│ Security Layers │
├─────────────────────────────────────────────────────┤
│ 1. Transport Layer (HTTPS in production) │
│ 2. CORS (Cross-Origin Resource Sharing) │
│ 3. Helmet.js (Security headers) │
│ 4. Request Size Limits (10MB) │
│ 5. JWT Authentication │
│ 6. Input Validation (AJV JSON Schema) │
│ 7. SQL Injection Protection (Sequelize ORM) │
│ 8. Password Hashing (bcryptjs) │
│ 9. Error Sanitization (no stack traces in prod) │
└─────────────────────────────────────────────────────┘
Security Headers (Helmet.js)
Automatically applied headers:
-
X-DNS-Prefetch-Control -
X-Frame-Options -
X-Content-Type-Options -
X-XSS-Protection -
Strict-Transport-Security(HTTPS only)
CORS Configuration
app.use(cors()); // Allow all origins (configure for production)
// Custom CORS for specific resources
app.use((req, res, next) => {
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
next();
});
Production CORS (recommended):
const corsOptions = {
origin: [
"https://yourdomain.com",
"https://app.yourdomain.com"
],
credentials: true
};
app.use(cors(corsOptions));
SQL Injection Prevention
Using Sequelize ORM with parameterized queries:
// ✓ Safe - parameterized
User.findOne({
where: { email: userInput }
});
// ✗ Unsafe - raw query without parameters
sequelize.query(`SELECT * FROM users WHERE email = '${userInput}'`);
// ✓ Safe - raw query with parameters
sequelize.query(
"SELECT * FROM users WHERE email = :email",
{
replacements: { email: userInput },
type: QueryTypes.SELECT
}
);
Password Security
const bcrypt = require("bcryptjs");
// Hashing (on user creation)
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
// Verification (on login)
const isValid = await bcrypt.compare(password, user.password);
Scalability Considerations
Horizontal Scaling
Stateless Design:
- JWT tokens (no server-side sessions)
- No in-memory state (use database/cache)
- Load balancer compatible
Database Connection Pooling:
- Limited connections per instance
- Reuse existing connections
- Automatic cleanup of idle connections
Performance Optimization
Database Queries:
- Use indexes on frequently queried fields
- Limit result sets (pagination)
- Use
selectto fetch only needed fields - Avoid N+1 queries (use
includefor joins)
Caching (future consideration):
// Example with Redis
const redis = require("redis");
const client = redis.createClient();
// Cache frequently accessed data
const cached = await client.get(key);
if (cached) return JSON.parse(cached);
const data = await database.query();
await client.setex(key, 3600, JSON.stringify(data));
Request Rate Limiting (future consideration):
const rateLimit = require("express-rate-limit");
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // Limit each IP to 100 requests per windowMs
});
app.use("/api/", limiter);
Monitoring & Logging
Current Logging:
- Morgan for HTTP request logging
- Colored console output for readability
- Sequelize query logging
Production Logging (recommended):
- Use Winston or Bunyan
- Log to files/external service
- Structured logging (JSON format)
- Different log levels (error, warn, info, debug)
Metrics (future consideration):
- Response times
- Error rates
- Database query performance
- Active connections
- Memory/CPU usage
Document Version: 1.0.0
Last Updated: 2026-02-04