Initializing
Back to Projects
Year2025
DomainFullstack
AccessPrivate Repo
Complexity8.5 / 10
Video DemoArchitecture DocsScreenshots
TypeScriptNestJSNext.jsReact NativeExpoPostgreSQLPrismaRedisBullMQMeiliSearchMinIOJWTWebAuthnSocket.IOTailwind CSS
FullstackProduction

EMS Universe — Enterprise Management System

A production-grade full-stack ERP platform with NestJS backend, Next.js admin portal, and React Native mobile app. Features attendance geofencing, payroll, performance 360°, and disaster recovery.

User Roles0
Platform Apps0
Database Models0+
Queue WorkersBullMQ
EncryptionAES-0-GCM
Search EngineMeiliSearch

Table of Contents


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.

Parsing system architecture diagram...

Three Application Architecture

AppTechnologyPurpose
apiNestJS 11 + PrismaRESTful backend, WebSockets, BullMQ queues
webNext.js 15 App RouterAdmin, Management, Accountant, Employee portals
mobileReact Native + ExpoNative Android/iOS companion app

Component Responsibilities

LayerComponentResponsibility
BackendNestJS APIAll business logic, auth, database operations
DatabasePostgreSQL + PrismaPersistent storage, migrations
Cache/QueueRedis + BullMQCaching, async job processing
SearchMeiliSearchSub-millisecond employee indexing
StorageMinIODocument and payslip object storage
MonitoringSentryFull-stack error tracking
FrontendNext.js WebMulti-portal SSR with PWA
MobileReact NativeNative iOS/Android with offline support

Tech Stack

Core Infrastructure

LayerTechnology
MonorepoNx with pnpm workspaces
Package Managerpnpm
LanguageTypeScript (strict, zero any)
DatabasePostgreSQL 16 + Prisma ORM
Cache/QueueRedis + BullMQ
SearchMeiliSearch
Object StorageMinIO (S3-compatible)
MonitoringSentry

Backend (apps/api)

TechnologyVersionUsage
NestJS11Core framework, DI, modules
Prisma6ORM, schema migrations
Passport.js + JWTAuthentication, token refresh
@nestjs/swagger11OpenAPI 3.0 documentation
nestjs-pino4Structured JSON logging
@nestjs/websockets11Real-time DR terminal feed
@simplewebauthn/server13WebAuthn/Passkey registration
BullMQ5Async job queues
PDFKit0.18Payslip & ID Card generation
ExcelJS4Bulk CSV/XLSX import
AES-256-GCMPII field-level encryption

Frontend (apps/web)

TechnologyVersionUsage
Next.js15App Router, SSR, PWA
React18/19UI framework
Shadcn UIAccessible component library
Tailwind CSS3Utility-first styling
Recharts3Charts: Radar, Bar, Area
Leaflet + leaflet.heat1.9Geofencing map + heatmap analytics
react-i18next15Internationalization (EN/HI)
Socket.io Client4DR War Room live terminal

Mobile (apps/mobile)

TechnologyVersionUsage
React Native0.77+Native UI framework
Expo SDK54/55Managed workflow, build tooling
expo-secure-store55Encrypted JWT storage
expo-sqlite55Offline attendance queue
expo-location55GPS geofencing
expo-task-managerBackground geofence monitoring
expo-local-authentication55Biometric login
expo-notifications55Push notifications (FCM)
Zustand5State management

Nx Monorepo Structure

code
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 config

Key Engineering Decisions

1. Nx Monorepo for Shared Type Safety

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

ModuleFeatures
authJWT access + refresh, WebAuthn/Passkey, bcrypt hashing
usersUser CRUD, role management, profile updates
employeesFull employee lifecycle, PII encryption, bulk import
applicantsApplication tracking, onboarding workflow
attendanceGeofenced check-in/out, Haversine validation, history
leave-requestsLeave applications, approval workflow, balance tracking
payrollPro-rata calculation, PDF payslip generation, history
performance360° reviews (self, peer, manager, direct-report)
documentsPDF ID cards, payslips, MinIO storage
bulk-importExcel/CSV ingestion with validation
notificationsEmail via Nodemailer, in-app notifications
analyticsExecutive dashboards, heatmaps, trend charts
searchMeiliSearch sync for sub-millisecond queries
drDisaster recovery backup jobs via BullMQ
auditImmutable audit trail for all mutations

Database Schema

Core Models

ModelDescription
UserAuthentication, roles, refresh tokens
AuthenticatorWebAuthn credentials for passkey login
EmployeeFull employee profile (encrypted PII)
ApplicationApplicant records and onboarding flow
AttendanceGeofenced check-in/out records
LeaveRequestLeave applications and approvals
SalaryRecordMonthly payroll snapshots
PerformanceReview360° review entries with ReviewType enum
AuditLogSystem-wide forensic audit trail
NotificationUser notifications
SystemConfigRuntime configuration including geofence coordinates
DepartmentOrganizational hierarchy
IdCardGenerated ID card metadata
RejectedApplicantPermanently rejected applications

Key Schema Relationships

prisma
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

typescript
// 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

FeatureDescription
PWA Supportnext-pwa for offline capability
Role-Based RenderingConditional UI based on UserRole enum
Geofence MapLeaflet interactive map for office coordinates
Attendance HeatmapLeaflet.heat for attendance visualization
Performance RadarRecharts Radar chart for 360° reviews
PDF GenerationClient-side PDF preview before download
InternationalizationEN/HI with react-i18next
Real-time UpdatesSocket.io for DR backup log streaming

Mobile: React Native App

typescript
// 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

FeatureImplementation
Biometric Loginexpo-local-authentication for fingerprint/face
Geofencingexpo-task-manager background region monitoring
Offline Modeexpo-sqlite queue with auto-sync
Push Notificationsexpo-notifications with FCM
Secure Storageexpo-secure-store for JWT tokens
Document Downloadsexpo-file-system + expo-sharing for payslips
Root Detectionexpo-device blocks compromised devices

Background Geofence Task

typescript
// 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)

RoleClearanceAccess Scope
DEVELOPERLevel 0System diagnostics, health terminal, all routes
ADMINLevel 1Full organizational oversight, user management
MANAGEMENTLevel 2Leave/applicant queues, team analytics
ACCOUNTANTLevel 3Payroll, financial cycles, reports
EMPLOYEELevel 4Self-service attendance, leave, profile
APPLICANTLevel 5Application status only

Security Measures

  1. JWT Access + Refresh Tokens: 15-minute access token, 7-day refresh token with rotation
  2. WebAuthn/Passkey: Level 0-2 accounts can register biometric credentials
  3. Field-Level Encryption: AES-256-GCM for Aadhaar, PAN, Bank Details
  4. Rate Limiting: BullMQ-based rate limiting per user role
  5. Audit Trail: Every mutation logged with actor, timestamp, target
  6. Root/Jailbreak Detection: Mobile app blocks compromised devices in production
  7. CORS: Strict origin allowlist
  8. Helmet: Security headers on all API responses

Real-time Features

Socket.io Integration

typescript
// 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

typescript
// 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

yaml
# 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:
      - api

Environment Variables

bash
# 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.

Submit a Technical Suggestion