Table of Contents
- The Challenge
- Architecture & Solution
- Tech Stack
- Nx Monorepo Structure
- Key Engineering Decisions
- Backend: API Modules
- Database Schema
- Frontend: Web Dashboard
- Mobile: React Native App
- Security Architecture
- Real-time Features
- Disaster Recovery
- Deployment
- Roadmap
The Challenge
Building a comprehensive enterprise management system requires addressing complex organizational needs:
- Fragmented employee data: Employee information, attendance, payroll, and performance reviews live in separate systems with no unified view
- Manual attendance tracking: Traditional punch cards or standalone attendance systems lack geofencing capabilities and offer no real-time visibility
- Complex payroll calculations: Manual salary processing is error-prone, especially with pro-rata calculations based on attendance
- Disconnected hiring workflow: Applicant tracking, onboarding, and employee conversion lack automation
- Security concerns: PII data like Aadhaar, PAN, and bank details require field-level encryption
- Limited mobile access: Desktop-only systems fail to provide field employees with attendance and leave management on-the-go
The requirement: Build a unified ERP platform that manages the entire employee lifecycle — from applicant onboarding to payroll disbursement, geofenced attendance, performance reviews, and executive analytics — with a Next.js web portal, React Native mobile app, and NestJS backend.
Architecture & Solution
EMS Universe is built on an Nx Monorepo architecture housing three production applications connected through shared TypeScript types and utilities.
Three Application Architecture
| App | Technology | Purpose |
|---|---|---|
| api | NestJS 11 + Prisma | RESTful backend, WebSockets, BullMQ queues |
| web | Next.js 15 App Router | Admin, Management, Accountant, Employee portals |
| mobile | React Native + Expo | Native Android/iOS companion app |
Component Responsibilities
| Layer | Component | Responsibility |
|---|---|---|
| Backend | NestJS API | All business logic, auth, database operations |
| Database | PostgreSQL + Prisma | Persistent storage, migrations |
| Cache/Queue | Redis + BullMQ | Caching, async job processing |
| Search | MeiliSearch | Sub-millisecond employee indexing |
| Storage | MinIO | Document and payslip object storage |
| Monitoring | Sentry | Full-stack error tracking |
| Frontend | Next.js Web | Multi-portal SSR with PWA |
| Mobile | React Native | Native iOS/Android with offline support |
Tech Stack
Core Infrastructure
| Layer | Technology |
|---|---|
| Monorepo | Nx with pnpm workspaces |
| Package Manager | pnpm |
| Language | TypeScript (strict, zero any) |
| Database | PostgreSQL 16 + Prisma ORM |
| Cache/Queue | Redis + BullMQ |
| Search | MeiliSearch |
| Object Storage | MinIO (S3-compatible) |
| Monitoring | Sentry |
Backend (apps/api)
| Technology | Version | Usage |
|---|---|---|
| NestJS | 11 | Core framework, DI, modules |
| Prisma | 6 | ORM, schema migrations |
| Passport.js + JWT | — | Authentication, token refresh |
| @nestjs/swagger | 11 | OpenAPI 3.0 documentation |
| nestjs-pino | 4 | Structured JSON logging |
| @nestjs/websockets | 11 | Real-time DR terminal feed |
| @simplewebauthn/server | 13 | WebAuthn/Passkey registration |
| BullMQ | 5 | Async job queues |
| PDFKit | 0.18 | Payslip & ID Card generation |
| ExcelJS | 4 | Bulk CSV/XLSX import |
| AES-256-GCM | — | PII field-level encryption |
Frontend (apps/web)
| Technology | Version | Usage |
|---|---|---|
| Next.js | 15 | App Router, SSR, PWA |
| React | 18/19 | UI framework |
| Shadcn UI | — | Accessible component library |
| Tailwind CSS | 3 | Utility-first styling |
| Recharts | 3 | Charts: Radar, Bar, Area |
| Leaflet + leaflet.heat | 1.9 | Geofencing map + heatmap analytics |
| react-i18next | 15 | Internationalization (EN/HI) |
| Socket.io Client | 4 | DR War Room live terminal |
Mobile (apps/mobile)
| Technology | Version | Usage |
|---|---|---|
| React Native | 0.77+ | Native UI framework |
| Expo SDK | 54/55 | Managed workflow, build tooling |
| expo-secure-store | 55 | Encrypted JWT storage |
| expo-sqlite | 55 | Offline attendance queue |
| expo-location | 55 | GPS geofencing |
| expo-task-manager | — | Background geofence monitoring |
| expo-local-authentication | 55 | Biometric login |
| expo-notifications | 55 | Push notifications (FCM) |
| Zustand | 5 | State management |
Nx Monorepo Structure
ems/
├── apps/
│ ├── api/ # NestJS Backend
│ │ └── src/
│ │ ├── common/ # Guards, decorators, interceptors
│ │ ├── config/ # Environment config service
│ │ ├── database/ # Prisma schema, service, seeders
│ │ └── modules/
│ │ ├── auth/ # JWT, Passport, WebAuthn
│ │ ├── users/ # User CRUD
│ │ ├── employees/ # Employee lifecycle
│ │ ├── applicants/ # Onboarding flow
│ │ ├── attendance/ # Geofenced check-in/out
│ │ ├── audit/ # Audit trail
│ │ ├── leave-requests/
│ │ ├── payroll/
│ │ ├── performance/ # 360° reviews
│ │ ├── documents/ # PDF generation
│ │ ├── bulk-import/ # Excel/CSV ingestion
│ │ ├── notifications/
│ │ ├── analytics/
│ │ ├── search/ # MeiliSearch
│ │ ├── dr/ # Disaster recovery jobs
│ │ └── queues/ # BullMQ queue definitions
│ │
│ ├── web/ # Next.js Frontend
│ │ └── src/
│ │ ├── app/
│ │ │ ├── (admin)/ # ADMIN + DEVELOPER routes
│ │ │ ├── (management)/
│ │ │ ├── (accountant)/
│ │ │ ├── (employee)/
│ │ │ └── (public)/ # Login, register, apply
│ │ ├── components/ # Shared UI components
│ │ ├── context/ # Auth context
│ │ ├── lib/ # api.ts, env.ts, utils
│ │ └── i18n/ # Translation files (en/hi)
│ │
│ └── mobile/ # React Native + Expo
│ └── src/
│ ├── app/ # App.tsx entry
│ ├── screens/ # Feature screens
│ ├── navigation/ # Tab + Stack navigator
│ ├── store/ # Zustand state
│ ├── hooks/ # useSync, useNotifications
│ ├── tasks/ # GeofenceTask (background)
│ ├── lib/ # Axios API client
│ └── components/ # Shared native components
│
├── packages/ # Shared libraries
├── docs/ # Architecture documentation
├── infra/k8s/ # Kubernetes manifests
├── scripts/ # Deployment & test utilities
├── deploy.sh # Production deployment script
├── start.sh # Local dev launcher
├── docker-compose.yml # Local dev containers
└── nx.json # Nx workspace configKey Engineering Decisions
1. Nx Monorepo for Shared Type Safety
// packages/shared-types/src/index.ts
export enum UserRole {
DEVELOPER = 'DEVELOPER',
ADMIN = 'ADMIN',
MANAGEMENT = 'MANAGEMENT',
ACCOUNTANT = 'ACCOUNTANT',
EMPLOYEE = 'EMPLOYEE',
APPLICANT = 'APPLICANT',
}
export interface Employee {
id: string;
userId: string;
departmentId: string;
designation: string;
doj: Date;
// Encrypted fields
aadhaar: string;
pan: string;
bankAccount: string;
bankIfsc: string;
}
export interface Attendance {
id: string;
employeeId: string;
checkIn: Date;
checkOut: Date | null;
checkInLocation: { lat: number; lng: number };
checkOutLocation: { lat: number; lng: number } | null;
geofenceValid: boolean;
}2. JWT Authentication with Refresh Token Rotation
// modules/auth/auth.service.ts
async function validateUser(email: string, password: string) {
const user = await this.userService.findByEmail(email);
if (!user || !await bcrypt.compare(password, user.password)) {
throw new UnauthorizedException('Invalid credentials');
}
// Generate tokens
const accessToken = this.jwtService.sign({ sub: user.id, role: user.role });
const refreshToken = this.jwtService.sign({ sub: user.id }, { expiresIn: '7d' });
// Hash and store refresh token
const hashedRefresh = await bcrypt.hash(refreshToken, 10);
await this.userService.updateRefreshToken(user.id, hashedRefresh);
return { accessToken, refreshToken };
}
async function refreshToken(oldRefresh: string) {
const decoded = this.jwtService.verify(oldRefresh);
const user = await this.userService.findById(decoded.sub);
if (!user || !user.refreshToken) throw new UnauthorizedException();
if (!await bcrypt.compare(oldRefresh, user.refreshToken)) {
throw new UnauthorizedException('Invalid refresh token');
}
// Rotate: generate new tokens
return this.generateTokens(user);
}3. Geofenced Attendance with Haversine Formula
// modules/attendance/attendance.service.ts
const RADIUS_METERS = 100; // Office geofence radius
function haversineDistance(lat1: number, lng1: number, lat2: number, lng2: number): number {
const R = 6371e3; // Earth's radius in meters
const φ1 = lat1 * Math.PI / 180;
const φ2 = lat2 * Math.PI / 180;
const Δφ = (lat2 - lat1) * Math.PI / 180;
const Δλ = (lng2 - lng1) * Math.PI / 180;
const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
Math.cos(φ1) * Math.cos(φ2) *
Math.sin(Δλ/2) * Math.sin(Δλ/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c; // Distance in meters
}
async function validateCheckIn(employeeId: string, location: { lat: number; lng: number }) {
const config = await this.configService.getOfficeLocation();
const distance = haversineDistance(
location.lat, location.lng,
config.lat, config.lng
);
return distance <= RADIUS_METERS;
}4. AES-256-GCM Field-Level Encryption
// lib/encryption.ts
import crypto from 'crypto';
const ALGORITHM = 'aes-256-gcm';
const IV_LENGTH = 12;
const TAG_LENGTH = 16;
export function encryptField(plaintext: string, key: Buffer): string {
const iv = crypto.randomBytes(IV_LENGTH);
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
const encrypted = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final(),
]);
const tag = cipher.getAuthTag();
return Buffer.concat([iv, tag, encrypted]).toString('base64');
}
// Encrypt PII in employee service
async function createEmployee(data: CreateEmployeeDto) {
const key = crypto.scryptSync(process.env.ENCRYPTION_KEY, 'salt', 32);
return this.prisma.employee.create({
data: {
...data,
aadhaar: encryptField(data.aadhaar, key),
pan: encryptField(data.pan, key),
bankAccount: encryptField(data.bankAccount, key),
bankIfsc: encryptField(data.bankIfsc, key),
},
});
}5. WebAuthn/Passkey Registration
// modules/auth/webauthn.service.ts
async function generateRegistrationOptions(user: User) {
const options = await generateRegistrationOptions({
rpName: 'EMS Universe',
rpID: process.env.RP_ID,
userID: user.id,
userName: user.email,
timeout: 60000,
attestation: 'none',
supportedAlgorithms: [-7, -257],
});
// Store challenge in session
await this.redis.setex(`webauthn:challenge:${user.id}`, 300, options.challenge);
return options;
}
async function verifyRegistration(userId: string, response: AuthenticatorAttestationResponse) {
const challenge = await this.redis.get(`webauthn:challenge:${userId}`);
const verification = await verifyRegistrationResponse({
credential: response,
expectedChallenge: challenge,
expectedOrigin: process.env.ALLOWED_ORIGIN,
expectedRPID: process.env.RP_ID,
});
if (verification.verified) {
await this.prisma.authenticator.create({
data: {
userId,
credentialID: verification.credentialID,
credentialPublicKey: verification.credentialPublicKey,
counter: verification.counter,
},
});
}
return verification;
}Backend: API Modules
Core Modules
| Module | Features |
|---|---|
| auth | JWT access + refresh, WebAuthn/Passkey, bcrypt hashing |
| users | User CRUD, role management, profile updates |
| employees | Full employee lifecycle, PII encryption, bulk import |
| applicants | Application tracking, onboarding workflow |
| attendance | Geofenced check-in/out, Haversine validation, history |
| leave-requests | Leave applications, approval workflow, balance tracking |
| payroll | Pro-rata calculation, PDF payslip generation, history |
| performance | 360° reviews (self, peer, manager, direct-report) |
| documents | PDF ID cards, payslips, MinIO storage |
| bulk-import | Excel/CSV ingestion with validation |
| notifications | Email via Nodemailer, in-app notifications |
| analytics | Executive dashboards, heatmaps, trend charts |
| search | MeiliSearch sync for sub-millisecond queries |
| dr | Disaster recovery backup jobs via BullMQ |
| audit | Immutable audit trail for all mutations |
Database Schema
Core Models
| Model | Description |
|---|---|
User | Authentication, roles, refresh tokens |
Authenticator | WebAuthn credentials for passkey login |
Employee | Full employee profile (encrypted PII) |
Application | Applicant records and onboarding flow |
Attendance | Geofenced check-in/out records |
LeaveRequest | Leave applications and approvals |
SalaryRecord | Monthly payroll snapshots |
PerformanceReview | 360° review entries with ReviewType enum |
AuditLog | System-wide forensic audit trail |
Notification | User notifications |
SystemConfig | Runtime configuration including geofence coordinates |
Department | Organizational hierarchy |
IdCard | Generated ID card metadata |
RejectedApplicant | Permanently rejected applications |
Key Schema Relationships
model User {
id String @id @default(cuid())
email String @unique
password String
role UserRole
employee Employee? @relation(fields: [employeeId], references: [id])
employeeId String? @unique
refreshToken String? // bcrypt hashed
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Employee {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id])
departmentId String
department Department @relation(fields: [departmentId], references: [id])
designation String
doj DateTime
// Encrypted PII fields
aadhaar String // AES-256-GCM encrypted
pan String
bankAccount String
bankIfsc String
attendances Attendance[]
leaveRequests LeaveRequest[]
salaryRecords SalaryRecord[]
reviewsGiven PerformanceReview[] @relation("reviewer")
reviewsReceived PerformanceReview[] @relation("reviewee")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}Frontend: Web Dashboard
Multi-Portal Architecture
// Route-based layout segmentation
app/
├── (public)/ # Public routes
│ ├── login/
│ ├── register/
│ └── apply/ # Applicant portal
│
├── (admin)/ # ADMIN + DEVELOPER only
│ ├── dashboard/
│ ├── users/
│ ├── employees/
│ ├── system/dr # Disaster Recovery War Room
│ └── settings/
│
├── (management)/ # MANAGEMENT + ADMIN
│ ├── dashboard/
│ ├── applicants/
│ ├── leave-requests/
│ └── analytics/
│
├── (accountant)/ # ACCOUNTANT + ADMIN
│ ├── dashboard/
│ ├── payroll/
│ └── reports/
│
└── (employee)/ # EMPLOYEE + above
├── dashboard/
├── attendance/
├── leave/
└── profile/Key Features
| Feature | Description |
|---|---|
| PWA Support | next-pwa for offline capability |
| Role-Based Rendering | Conditional UI based on UserRole enum |
| Geofence Map | Leaflet interactive map for office coordinates |
| Attendance Heatmap | Leaflet.heat for attendance visualization |
| Performance Radar | Recharts Radar chart for 360° reviews |
| PDF Generation | Client-side PDF preview before download |
| Internationalization | EN/HI with react-i18next |
| Real-time Updates | Socket.io for DR backup log streaming |
Mobile: React Native App
Navigation Structure
// navigation/AppNavigator.tsx
const Tab = createBottomTabNavigator();
function AppNavigator() {
return (
<Tab.Navigator>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Attendance" component={AttendanceScreen} />
<Tab.Screen name="Leave" component={LeaveScreen} />
<Tab.Screen name="Profile" component={ProfileScreen} />
</Tab.Navigator>
);
}Mobile Features
| Feature | Implementation |
|---|---|
| Biometric Login | expo-local-authentication for fingerprint/face |
| Geofencing | expo-task-manager background region monitoring |
| Offline Mode | expo-sqlite queue with auto-sync |
| Push Notifications | expo-notifications with FCM |
| Secure Storage | expo-secure-store for JWT tokens |
| Document Downloads | expo-file-system + expo-sharing for payslips |
| Root Detection | expo-device blocks compromised devices |
Background Geofence Task
// tasks/GeofenceTask.ts
import * as TaskManager from 'expo-task-manager';
const GEOFENCE_TASK = 'background-geofence-task';
TaskManager.defineTask(GEOFENCE_TASK, ({ data, error }) => {
if (error) {
console.error('Geofence error:', error);
return;
}
if (data.region === 'OFFICE_GEOFENCE') {
const { eventType } = data;
if (eventType === 'enter') {
// Auto-trigger check-in
api.post('/attendance/check-in', { auto: true });
} else if (eventType === 'exit') {
// Prompt check-out
notifyUser('Ready to check out?');
}
}
});Security Architecture
Role-Based Access Control (6 Levels)
| Role | Clearance | Access Scope |
|---|---|---|
| DEVELOPER | Level 0 | System diagnostics, health terminal, all routes |
| ADMIN | Level 1 | Full organizational oversight, user management |
| MANAGEMENT | Level 2 | Leave/applicant queues, team analytics |
| ACCOUNTANT | Level 3 | Payroll, financial cycles, reports |
| EMPLOYEE | Level 4 | Self-service attendance, leave, profile |
| APPLICANT | Level 5 | Application status only |
Security Measures
- JWT Access + Refresh Tokens: 15-minute access token, 7-day refresh token with rotation
- WebAuthn/Passkey: Level 0-2 accounts can register biometric credentials
- Field-Level Encryption: AES-256-GCM for Aadhaar, PAN, Bank Details
- Rate Limiting: BullMQ-based rate limiting per user role
- Audit Trail: Every mutation logged with actor, timestamp, target
- Root/Jailbreak Detection: Mobile app blocks compromised devices in production
- CORS: Strict origin allowlist
- Helmet: Security headers on all API responses
Real-time Features
Socket.io Integration
// DR War Room WebSocket Gateway
@WebSocketGateway({ cors: { origin: '*' } })
export class DrGateway {
@SubscribeMessage('subscribe:dr-logs')
handleSubscribe(client: Socket) {
client.join('dr-logs');
}
async broadcastDrProgress(log: string) {
this.server.to('dr-logs').emit('dr:log', {
timestamp: new Date(),
message: log,
});
}
}
// Frontend consumption
useEffect(() => {
const socket = io('https://api.swaraniretreat.in');
socket.on('dr:log', (data) => setLogs(prev => [...prev, data]));
}, []);Disaster Recovery
Backup Workflow
// modules/dr/dr-backup.job.ts
@Processor('dr-backup')
export class DrBackupProcessor {
@Process()
async handleBackup(job: Job) {
const logger = job.log('Starting DR backup...');
// 1. Dump PostgreSQL
const pgDump = await this.exec(`pg_dump ${process.env.DATABASE_URL}`);
await this.minio.upload('dr-backups', `pg-${Date.now()}.sql`, pgDump);
logger.log('PostgreSQL dump complete');
// 2. Backup MinIO buckets
const buckets = await this.minio.listBuckets();
for (const bucket of buckets) {
await this.backupBucket(bucket.name);
}
logger.log('MinIO backup complete');
// 3. Notify completion
await this.notificationService.sendAdminAlert('DR Backup Complete');
logger.log('Backup job finished');
}
}DR War Room Features
- Big Red Button: Double-confirm dialog to trigger backup
- Real-time Log Streaming: Socket.io feed showing progress
- History: Previous backup timestamps and sizes
- Verification: Backup integrity checksums
Deployment
Production Stack
# docker-compose.prod.yml
services:
postgres:
image: postgres:16-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: ems
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ems_universe
redis:
image: redis:7-alpine
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
api:
build: ./apps/api
environment:
DATABASE_URL: postgresql://ems:${DB_PASSWORD}@postgres:5432/ems_universe
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
depends_on:
- postgres
- redis
web:
build: ./apps/web
depends_on:
- apiEnvironment Variables
# Required production secrets
DATABASE_URL=
REDIS_PASSWORD=
JWT_SECRET=
ENCRYPTION_KEY=
MINIO_ACCESS_KEY=
MINIO_SECRET_KEY=
SENTRY_DSN=
MEILISEARCH_KEY=Roadmap
Phase 1 — Core Foundation (Complete)
- NestJS API with Prisma
- PostgreSQL database
- Basic authentication
Phase 2 — Employee Management (Complete)
- Employee CRUD with encrypted PII
- Department hierarchy
- Bulk CSV import via ExcelJS
Phase 3 — Attendance System (Complete)
- Geofenced check-in/out
- Haversine validation
- Mobile background geofencing
Phase 4 — Leave Management (Complete)
- Leave application workflow
- Approval/rejection
- Balance tracking
Phase 5 — Payroll (Complete)
- Pro-rata salary calculation
- PDF payslip generation
- Payroll history
Phase 6 — Performance Reviews (Complete)
- 360° review system
- Radar chart visualization
- Goal tracking
Phase 7 — Disaster Recovery (Complete)
- BullMQ backup jobs
- Real-time log streaming
- MinIO storage
Phase 8 — Search & Analytics (Complete)
- MeiliSearch integration
- Executive dashboards
- Attendance heatmaps
Phase 9 — Mobile Refinement (In Progress)
- Offline queue improvements
- Push notification polish
Phase 10 — Advanced Features (Planned)
- AI-powered analytics
- Custom report builder
- Third-party integrations
Engineering Proof
Real-world validation, system demonstrations, and interface captures of the execution states.
System Demonstration
Video walkthrough detailing core logic, interactions, and system behaviors in action.
System Captures
Architecture Feedback
Spotted a potential optimization or antipattern? Let me know.
