Case Study

National Defense Academy

Developed an end-to-end visitor entry system for NDA Passing Out Parade, managing invitation-code-based guest onboarding, pavilion and zone seat allotment, and gate-level entry-exit control for large-scale ceremonial events.

Real-World Deployment

This system was built and deployed for NDA's Passing Out Parade 2026 at Khadakwasla, Pune — coordinating hundreds of military guests, VIP delegations, and officers across multiple stadium pavilions with strict cryptographic access control.

Core Feature Overview

🎟️

HMAC QR Ticketing

Tamper-proof invitation passes are generated with AES-256 encrypted payloads and HMAC-signed token strings to block forgery at gate level.

Complete Project Structure

root
NDA-pass-gen-main/
backend/
src/
config/
db.js— MongoDB Atlas connection with Mongoose
cloudinary.js— Cloudinary photo upload config
env.js— Validated .env extraction
controllers/
auth.controller.js— Login, register, OTP verify
booking.controller.js— Seat reservation & confirmation
checkinLog.controller.js— QR scan & manual gate check-in
invitationCode.controller.js— Code validation & eligibility checks
pass.controller.js— Pass lifecycle management
qrTicket.controller.js— Encrypted QR ticket generation
allotment.controller.js— Pass-to-seat allotment handlers
report.controller.js— Pavilion gate summary exports
systemConfig.controller.js— Admin event & quota config
auditLog.controller.js— Security audit trail queries
models/
User.model.js— Roles: guest, officer, admin, gate-staff
Booking.model.js— Seat reservation with status machine
Seat.model.js— Row/column slots per pavilion block
Pavilion.model.js— Stadium zones with capacity limits
QRTicket.model.js— Encrypted ticket with scan counts
OTP.model.js— OTP hash, expiry, attempt tracking
Pass.model.js— Pass quota, type, eligibility rules
Allotment.model.js— Pass-guest assignment
CheckinLog.model.js— Entry/exit transaction log
AuditLog.model.js— Immutable security audit trail
InvitationCode.model.js— Military code pool and status
services/
otp.service.js— OTP send/verify/resend/invalidate
pass.service.js— Quota check, eligibility, increment/decrement
auth.service.js— JWT refresh token pair management
booking.service.js— Atomic seat claim with conflict guard
checkinLog.service.js— Gate scan, manual override, reports
email.service.js— Nodemailer SMTP transports
pavilion.service.js— Stadium layout queries
auditLog.service.js— Audit trail creation
middlewares/
auth.middleware.js— Bearer JWT verification
role.middleware.js— RBAC gate for route protection
rateLimiter.middleware.js— OTP and login rate guards
validate.middleware.js— Joi schema validation pipe
error.middleware.js— Global error handler with Winston
routes/— REST API route definitions
utils/— QR compiler, crypto helpers, logger
server.js— Express + Docker entrypoint
Dockerfile— Multi-stage Node image
docker-compose.yml
frontend/
src/
components/— Seat grid, QR scanner modal, OTP form
pages/— Dashboard, ticket download, gate scanner

Guest Entry & Gate Access Flow

1

Invitation Code Validation

Guests enter their unique military invitation code. The server queries the `InvitationCode` collection, checks quota limits, user role eligibility (`officer/cadet/guest`), and confirms the code has not already been claimed.
2

OTP Multi-Factor Verification

Identity verification triggers both channels. An OTP is generated with a 10-minute TTL and stored as a hashed value in the `OTP` model. Nodemailer dispatches to the registered email; Twilio dispatches via SMS. Attempt-limiting is enforced at 5 tries before the code locks.
3

Pavilion Seat Selection

The guest selects a seat from an interactive stadium grid. The backend validates against available row-column slots for their assigned pavilion zone. An atomic MongoDB `findOneAndUpdate` with `$set: { isBooked: true }` prevents race conditions on popular seats.
4

Booking Confirmation & Auto-Number

A `Booking` document is created with status `pending`. The `pre('validate')` hook on the Booking model auto-generates a unique booking number in the format `NDA-BKG-{timestamp}-{random4}` before committing to the database.
5

Encrypted QR Ticket Generation

Once confirmed, the system generates a QR payload by AES-256-CBC encrypting the guest ID + seat code, then computing an HMAC-SHA256 signature over the ciphertext. The QR ticket is mailed to the guest and stored in the `QRTicket` model.
6

Gate Check-In via QR Scan

Gate staff scan QR codes. The `checkinLog.service.js` calls `qrTicketService.scanQRTicket(qrData)` to validate the signature. Entry or exit is determined by scan-count parity — odd scans = entry, even scans = exit. All events are logged to `CheckinLog` with the gate number and scanned-by officer ID.

MongoDB Data Models

Booking Model — Seat Reservation State Machine

javascript
// backend/src/models/Booking.model.js
import mongoose from 'mongoose';

const BOOKING_STATUS = {
  PENDING:   'pending',    // seat reserved, awaiting confirmation
  CONFIRMED: 'confirmed',  // seat permanently booked
  CANCELLED: 'cancelled',
  COMPLETED: 'completed',  // visitor checked in on event day
  NO_SHOW:   'no_show',
};

const bookingSchema = new mongoose.Schema({
  bookingNumber: { type: String, unique: true, index: true },
  user:          { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  allotment:     { type: mongoose.Schema.Types.ObjectId, ref: 'Allotment' },
  seat:          { type: mongoose.Schema.Types.ObjectId, ref: 'Seat', required: true },
  pavilion:      { type: mongoose.Schema.Types.ObjectId, ref: 'Pavilion', required: true },
  eventName:     { type: String, default: 'NDA Passing Out Parade 2026' },
  eventDate:     { type: Date, default: () => new Date('2026-09-12T08:30:00+05:30') },
  eventVenue:    { type: String, default: 'NDA Khadakwasla, Pune' },
  status:        { type: String, enum: Object.values(BOOKING_STATUS), default: 'pending' },
  statusHistory: [{
    status: String, updatedBy: mongoose.Schema.Types.ObjectId,
    updatedAt: { type: Date, default: Date.now }, remark: String, _id: false
  }],
  qrTicket:     { type: mongoose.Schema.Types.ObjectId, ref: 'QRTicket' },
}, { timestamps: true });

// Partial unique index: only one active booking per seat slot
bookingSchema.index(
  { seat: 1 },
  {
    unique: true,
    partialFilterExpression: {
      status: { $in: ['pending', 'confirmed', 'completed', 'no_show'] }
    }
  }
);

// Auto-generate booking number before validation
bookingSchema.pre('validate', function (next) {
  if (!this.bookingNumber) {
    const ts  = Date.now().toString().slice(-6);
    const rnd = Math.floor(1000 + Math.random() * 9000);
    this.bookingNumber = `NDA-BKG-${ts}-${rnd}`;
  }
  next();
});

OTP Service — Attempt-Limited Verification

javascript
// backend/src/services/otp.service.js
const sendOTP = async ({ userId, identifier, purpose, via = 'email', metadata = {} }) => {
  const { code } = await OTP.createOTP({ userId, identifier, purpose, expiresInMinutes: 10, metadata });

  if (via === 'email')     await emailService.sendOTPEmail(identifier, code, purpose);
  else if (via === 'sms')  await smsService.sendOTPSMS(identifier, code, purpose);

  return { message: `OTP sent to ${identifier}` };
};

const verifyOTP = async ({ userId, code, purpose }) => {
  const otp = await OTP.findOne({ user: userId, purpose, isUsed: false }).select('+code');

  if (!otp)                          throw new Error('OTP not found or already used');
  if (new Date() > otp.expiresAt)    throw new Error('OTP has expired');
  if (otp.attempts >= otp.maxAttempts) throw new Error('Maximum OTP attempts exceeded');
  if (otp.code !== code) {
    otp.attempts += 1;
    await otp.save();
    throw new Error(`Invalid OTP. ${otp.maxAttempts - otp.attempts} attempts remaining`);
  }

  otp.isUsed = true;
  otp.usedAt = new Date();
  await otp.save();
  return { verified: true };
};

Gate Check-In Logic

The gate scan service automatically detects direction based on scan count parity. This means the same QR code handles both entry and exit without any UI input from gate staff:

javascript
// backend/src/services/checkinLog.service.js
const processCheckin = async ({ qrData, scannedBy, pavilionId, gate, metadata }) => {
  let ticket, status, failureReason;

  try {
    ticket = await qrTicketService.scanQRTicket(qrData);
    status = 'success';
  } catch (err) {
    const failedTicket = await QRTicket.findOne({ qrData });
    if (!failedTicket) throw new Error('Invalid QR data — ticket not found');
    ticket = failedTicket;
    status = 'failed';
    failureReason = err.message;
  }

  // Odd scan count = exit, Even scan count = entry (starts at 0)
  const type = ticket.scanCount % 2 === 1 ? 'exit' : 'entry';

  const log = await CheckinLog.create({
    user: ticket.user, qrTicket: ticket._id, allotment: ticket.allotment,
    pavilion: pavilionId || ticket.pavilion, type, method: 'qr_scan',
    status, scannedBy, scannedAt: new Date(), gate, failureReason, metadata,
  });

  return log.populate(['user', 'qrTicket', 'pavilion', 'scannedBy']);
};

Pass Eligibility & Quota Guard

javascript
// backend/src/services/pass.service.js
const checkEligibility = async (passId, user) => {
  const pass = await Pass.findById(passId);
  if (!pass)                      throw new Error('Pass not found');
  if (!pass.isValid)              throw new Error('Pass is not currently active or valid');
  if (pass.quota.remaining <= 0)  throw new Error('Pass quota exhausted');

  const { eligibility } = pass;
  if (eligibility.roles.length && !eligibility.roles.includes(user.role))
    throw new Error(`This pass is not available for role: ${user.role}`);

  if (eligibility.allowedRanks.length && !eligibility.allowedRanks.includes(user.rank))
    throw new Error(`This pass is not available for rank: ${user.rank}`);

  return { eligible: true, pass };
};

const decrementQuota = async (passId) => {
  const pass = await Pass.findById(passId);
  pass.quota.remaining -= 1;
  pass.quota.usedCount  += 1;
  if (pass.quota.remaining === 0) pass.status = 'exhausted';
  await pass.save();
  return pass;
};

Gate Report Aggregation

The gate dashboard displays a real-time summary of entries, exits, and unique visitors per pavilion using a single MongoDB aggregation pipeline:

javascript
const getGateReport = async ({ pavilionId, from, to }) => {
  const [summary] = await CheckinLog.aggregate([
    { $match: { pavilion: pavilionId, status: 'success', scannedAt: { $gte: new Date(from), $lte: new Date(to) } } },
    {
      $group: {
        _id: null,
        totalScans:   { $sum: 1 },
        totalEntries: { $sum: { $cond: [{ $eq: ['$type', 'entry'] }, 1, 0] } },
        totalExits:   { $sum: { $cond: [{ $eq: ['$type', 'exit'] }, 1, 0] } },
        uniqueUsers:  { $addToSet: '$user' },
      },
    },
    {
      $project: {
        _id: 0, totalScans: 1, totalEntries: 1, totalExits: 1,
        uniqueUsers: { $size: '$uniqueUsers' },
      },
    },
  ]);

  return summary || { totalScans: 0, totalEntries: 0, totalExits: 0, uniqueUsers: 0 };
};

Deployment Note

The backend is containerized using Docker with a multi-stage Node.js build. The compose file connects MongoDB and the Express API. The `.env.example` documents all required secrets including `ENCRYPTION_KEY`, `HMAC_KEY`, `TWILIO_*`, `NODEMAILER_*`, and `JWT_*` pairs.