Initializing
Back to Projects
Year2024
DomainFullstack
AccessOpen Source
Complexity0 / 10
Next.jsNode.jsExpressMariaDBSequelizeRazorpayiCalRBACDocker
FullstackArchived

Hotel Reservation System (FSHRS)

A full-stack hotel reservation system with Next.js, Node.js/Express, and MariaDB featuring real-time availability, payment integration, OTA synchronization, and white-labeling capabilities.

Parsing system architecture diagram...

Technology Stack

LayerTechnologyVersion/Details
FrontendNext.js14 (App Router)
UI FrameworkTailwind CSS + Shadcn UI-
BackendNode.js + Express^4.18.2
DatabaseMariaDBvia Docker
ORMSequelize^6.35.1
AuthenticationJWT + bcryptjs-
PaymentRazorpay^2.9.6
EmailNodemailer^7.0.10
Schedulernode-cron^3.0.3
iCalnode-ical + ical-generator-
DockerDocker Compose-

Purpose and Philosophy

Full-Stack Hotel Reservation System (FSHRS) is a comprehensive hospitality management platform designed to provide hoteliers with a complete solution for managing room inventory, bookings, payments, and guest communication. The system follows a modern microservices-inspired architecture with clear separation between frontend and backend concerns.

The core philosophy centers on "Hospitality meets Technology" - creating an intuitive experience for guests while providing powerful management tools for hotel staff. The system is built to be deployment-ready with production-grade features including ACID-compliant transactions, role-based access control, and external system integrations.

Core Design Principles

  1. ACID Database Transactions: Using MariaDB InnoDB engine ensures reliable booking operations with row-level locking to prevent double-bookings.
  1. Inventory Locking Mechanism: During the checkout process, rooms are temporarily locked to prevent race conditions when multiple guests attempt to book the same room simultaneously.
  1. White-Label Ready: Complete branding customization enables multi-tenant deployments where different hotel brands can use the same codebase with distinct visual identities.
  1. OTA Synchronization: Native support for iCal format enables two-way synchronization with external platforms like Airbnb and Booking.com.
  1. Offline-First Guest Management: Client-side offline support with automatic synchronization allows guests to manage their bookings even without internet connectivity.

Architecture Deep Dive

Database Schema Design

The system uses a comprehensive relational database schema optimized for hospitality operations:

sql
-- Users Table with Role-Based Access
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(150) UNIQUE NOT NULL,
    password VARCHAR(255),
    role ENUM('guest', 'admin', 'developer') DEFAULT 'guest',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Rooms Table with Dynamic Pricing
CREATE TABLE rooms (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    slug VARCHAR(100) UNIQUE,
    type VARCHAR(50),
    base_price DECIMAL(10, 2) NOT NULL,
    offer_price DECIMAL(10, 2),
    coupon_code VARCHAR(50),
    default_adults INT DEFAULT 1,
    default_children INT DEFAULT 0,
    max_adults INT DEFAULT 2,
    max_children INT DEFAULT 0,
    extra_adult_cost DECIMAL(10, 2) DEFAULT 0,
    extra_child_cost DECIMAL(10, 2) DEFAULT 0,
    images JSON,
    amenities JSON,
    status ENUM('active', 'inactive') DEFAULT 'active'
);

-- Bookings Table with Composite Index
CREATE TABLE bookings (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    room_id INT,
    check_in DATE NOT NULL,
    check_out DATE NOT NULL,
    adults INT,
    children INT,
    total_price DECIMAL(10, 2) NOT NULL,
    status ENUM('pending', 'confirmed', 'cancelled', 'blocked') DEFAULT 'pending',
    payment_status ENUM('paid', 'unpaid', 'refunded') DEFAULT 'unpaid',
    payment_method VARCHAR(50),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_room_dates (room_id, check_in, check_out)
);

Booking Model (Sequelize)

javascript
// server/models/Booking.js
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');

const Booking = sequelize.define('Booking', {
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  checkIn: {
    type: DataTypes.DATEONLY,
    allowNull: false,
    field: 'check_in'
  },
  checkOut: {
    type: DataTypes.DATEONLY,
    allowNull: false,
    field: 'check_out'
  },
  adults: {
    type: DataTypes.INTEGER,
    defaultValue: 1
  },
  children: {
    type: DataTypes.INTEGER,
    defaultValue: 0
  },
  totalPrice: {
    type: DataTypes.DECIMAL(10, 2),
    allowNull: false,
    field: 'total_price'
  },
  status: {
    type: DataTypes.ENUM('pending', 'confirmed', 'cancelled', 'blocked'),
    defaultValue: 'pending'
  },
  paymentStatus: {
    type: DataTypes.ENUM('paid', 'unpaid', 'refunded'),
    defaultValue: 'unpaid',
    field: 'payment_status'
  },
  paymentMethod: {
    type: DataTypes.STRING(50),
    field: 'payment_method'
  },
  guestName: {
    type: DataTypes.STRING
  },
  guestEmail: {
    type: DataTypes.STRING
  },
  guestPhone: {
    type: DataTypes.STRING
  }
}, {
  tableName: 'bookings',
  underscored: true,
  timestamps: true
});

module.exports = Booking;

Availability Service Logic

The most critical function in the system - checking room availability using half-open interval logic:

javascript
// services/availabilityService.js
const { Op } = require('sequelize');
const Booking = require('../models/Booking');

async function checkAvailability(roomId, checkInDate, checkOutDate) {
  const conflictingBookings = await Booking.count({
    where: {
      roomId: roomId,
      status: { [Op.in]: ['confirmed', 'pending', 'blocked'] },
      [Op.and]: [
        { checkIn: { [Op.lt]: checkOutDate } },
        { checkOut: { [Op.gt]: checkInDate } }
      ]
    }
  });

  return conflictingBookings === 0;
}

async function getAvailableRooms(checkInDate, checkOutDate) {
  const allRooms = await Room.findAll({
    where: { status: 'active' }
  });

  const availableRooms = [];
  for (const room of allRooms) {
    const isAvailable = await checkAvailability(room.id, checkInDate, checkOutDate);
    if (isAvailable) {
      availableRooms.push(room);
    }
  }

  return availableRooms;
}

Inventory Locking

To prevent race conditions during checkout, the system implements inventory locking:

javascript
// server/services/inventoryLockService.js
const { Op } = require('sequelize');
const InventoryLock = require('../models/InventoryLock');

async function acquireLock(roomId, checkIn, checkOut, guestId) {
  const existingLock = await InventoryLock.findOne({
    where: {
      roomId,
      [Op.and]: [
        { checkIn: { [Op.lt]: checkOut } },
        { checkOut: { [Op.gt]: checkIn } }
      ],
      expiresAt: { [Op.gt]: new Date() }
    }
  });

  if (existingLock) {
    throw new Error('Room is currently being booked by another guest');
  }

  const lock = await InventoryLock.create({
    roomId,
    checkIn,
    checkOut,
    guestId,
    expiresAt: new Date(Date.now() + 15 * 60 * 1000) // 15 minute lock
  });

  return lock;
}

async function releaseLock(lockId) {
  await InventoryLock.destroy({ where: { id: lockId } });
}

Role-Based Access Control (RBAC)

User Roles and Permissions

RolePermissions
GuestBrowse rooms, make bookings, manage profile, view booking history
AdminAll guest permissions + manage bookings, rooms, customers, services, basic settings
DeveloperAll admin permissions + white-labeling, email/payment config, API/webhook management, factory reset

Authentication Flow

javascript
// server/controllers/authController.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');

async function login(req, res) {
  const { email, password } = req.body;
  
  const user = await User.findOne({ where: { email } });
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const isValid = await bcrypt.compare(password, user.password);
  if (!isValid) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const token = jwt.sign(
    { id: user.id, email: user.email, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: '7d' }
  );

  res.json({ 
    token, 
    user: { id: user.id, name: user.name, email: user.email, role: user.role }
  });
}

Middleware for Permission Checking

javascript
// server/middleware/authMiddleware.js
const jwt = require('jsonwebtoken');

function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    res.status(401).json({ error: 'Invalid token' });
  }
}

function authorize(...roles) {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    next();
  };
}

Payment Integration (Razorpay)

Payment Flow

javascript
// server/services/razorpayService.js
const Razorpay = require('razorpay');

const razorpay = new Razorpay({
  key_id: process.env.RAZORPAY_KEY_ID,
  key_secret: process.env.RAZORPAY_KEY_SECRET
});

async function createPaymentOrder(booking) {
  const options = {
    amount: Math.round(booking.totalPrice * 100), // Razorpay expects paise
    currency: 'INR',
    receipt: `booking_${booking.id}`,
    notes: {
      bookingId: booking.id,
      guestEmail: booking.guestEmail
    }
  };

  const order = await razorpay.orders.create(options);
  return order;
}

async function verifyPayment(paymentId, orderId, signature) {
  const crypto = require('crypto');
  const expectedSignature = crypto
    .createHmac('sha256', process.env.RAZORPAY_KEY_SECRET)
    .update(orderId + '|' + paymentId)
    .digest('hex');

  return expectedSignature === signature;
}

Razorpay Controller

javascript
// server/controllers/razorpayController.js
router.post('/create-order', authenticate, async (req, res) => {
  try {
    const { bookingId } = req.body;
    const booking = await Booking.findByPk(bookingId);
    
    if (!booking) {
      return res.status(404).json({ error: 'Booking not found' });
    }

    const order = await razorpayService.createPaymentOrder(booking);
    res.json(order);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

router.post('/verify', authenticate, async (req, res) => {
  try {
    const { paymentId, orderId, signature, bookingId } = req.body;
    
    const isValid = await razorpayService.verifyPayment(paymentId, orderId, signature);
    if (!isValid) {
      return res.status(400).json({ error: 'Invalid payment signature' });
    }

    const booking = await Booking.findByPk(bookingId);
    booking.paymentStatus = 'paid';
    booking.status = 'confirmed';
    await booking.save();

    // Send confirmation email
    await emailService.sendBookingConfirmation(booking);

    res.json({ success: true, booking });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

OTA Synchronization (iCal)

Inbound Sync (Import from Airbnb/Booking.com)

javascript
// server/services/otaService.js
const nodeIcal = require('node-ical');
const Booking = require('../models/Booking');
const OtaLink = require('../models/OtaLink');

async function syncCalendars() {
  const links = await OtaLink.findAll({ include: ['room'] });
  
  for (const link of links) {
    try {
      const events = await nodeIcal.async.parseURL(link.icalUrl);
      
      for (const key in events) {
        const event = events[key];
        if (event.type === 'VEVENT') {
          await syncOtaBooking(link.roomId, event);
        }
      }
    } catch (error) {
      console.error(`Failed to sync ${link.platformName}:`, error.message);
    }
  }
}

async function syncOtaBooking(roomId, event) {
  const existing = await Booking.findOne({
    where: {
      roomId,
      checkIn: event.start,
      source: 'ota_import'
    }
  });

  if (!existing) {
    await Booking.create({
      roomId,
      checkIn: event.start,
      checkOut: event.end,
      status: 'blocked',
      source: 'ota_import',
      guestName: 'OTA Import',
      totalPrice: 0
    });
  }
}

Outbound Sync (Export to OTAs)

javascript
// server/routes/icalRoutes.js
const ical = require('ical-generator');
const Booking = require('../models/Booking');

router.get('/room/:roomId/export', async (req, res) => {
  const { roomId } = req.params;
  
  const bookings = await Booking.findAll({
    where: {
      roomId,
      status: { [Op.in]: ['confirmed', 'blocked'] }
    }
  });

  const calendar = ical({ name: `Room ${roomId} Bookings` });
  
  bookings.forEach(booking => {
    calendar.createEvent({
      start: new Date(booking.checkIn),
      end: new Date(booking.checkOut),
      summary: `Booking #${booking.id}`,
      description: `Guest: ${booking.guestName}`
    });
  });

  res.type('text/calendar');
  calendar.serve(res);
});

Webhook System

Webhook Configuration

javascript
// server/models/ApiWebhook.js
const { DataTypes } = require('sequelize');

const ApiWebhook = sequelize.define('ApiWebhook', {
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  url: {
    type: DataTypes.STRING,
    allowNull: false
  },
  events: {
    type: DataTypes.JSON,
    defaultValue: ['booking.created', 'booking.updated']
  },
  secret: {
    type: DataTypes.STRING
  },
  isActive: {
    type: DataTypes.BOOLEAN,
    defaultValue: true
  }
});

Webhook Delivery Service

javascript
// server/services/webhookService.js
const crypto = require('crypto');

async function deliverWebhook(webhook, event, payload) {
  const signature = crypto
    .createHmac('sha256', webhook.secret)
    .update(JSON.stringify(payload))
    .digest('hex');

  const response = await fetch(webhook.url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Webhook-Signature': signature,
      'X-Webhook-Event': event
    },
    body: JSON.stringify(payload)
  });

  await WebhookLog.create({
    webhookId: webhook.id,
    event,
    payload: JSON.stringify(payload),
    responseStatus: response.status,
    deliveredAt: new Date()
  });

  return response.ok;
}

async function broadcastEvent(event, payload) {
  const webhooks = await ApiWebhook.findAll({
    where: { isActive: true }
  });

  for (const webhook of webhooks) {
    if (webhook.events.includes(event)) {
      await deliverWebhook(webhook, event, payload);
    }
  }
}

White-Labeling and Branding

Branding Settings Model

javascript
// server/models/BrandingSettings.js
const BrandingSettings = sequelize.define('BrandingSettings', {
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true
  },
  siteName: {
    type: DataTypes.STRING,
    field: 'site_name'
  },
  logoUrl: {
    type: DataTypes.STRING,
    field: 'logo_url'
  },
  primaryColor: {
    type: DataTypes.STRING,
    field: 'primary_color'
  },
  secondaryColor: {
    type: DataTypes.STRING,
    field: 'secondary_color'
  },
  fontFamily: {
    type: DataTypes.STRING,
    field: 'font_family'
  },
  faviconUrl: {
    type: DataTypes.STRING,
    field: 'favicon_url'
  }
});

Dynamic CSS Variable Application

javascript
// server/controllers/brandingController.js
router.get('/branding', (req, res) => {
  res.json({
    siteName: settings.siteName,
    cssVariables: {
      '--primary-color': settings.primaryColor,
      '--secondary-color': settings.secondaryColor,
      '--font-family': settings.fontFamily
    }
  });
});

Setup Wizard

Initial Configuration Flow

javascript
// server/controllers/setupController.js
router.post('/initialize', async (req, res) => {
  const { databaseConfig, adminUser, developerUser } = req.body;
  
  // Test database connection
  const sequelize = new Sequelize(
    databaseConfig.database,
    databaseConfig.username,
    databaseConfig.password,
    { host: databaseConfig.host, dialect: 'mysql' }
  );
  
  await sequelize.authenticate();
  
  // Run migrations
  await sequelize.sync();
  
  // Create admin user
  const admin = await User.create({
    name: adminUser.name,
    email: adminUser.email,
    password: await bcrypt.hash(adminUser.password, 10),
    role: 'admin'
  });
  
  // Create developer user
  const developer = await User.create({
    name: developerUser.name,
    email: developerUser.email,
    password: await bcrypt.hash(developerUser.password, 10),
    role: 'developer'
  });
  
  res.json({ success: true });
});

Key Features

1. Room Management

  • Multiple images and videos per room
  • Dynamic occupancy rules (adults/children limits)
  • Extra person cost configuration
  • Coupon code support
  • Service/amenity management
  • Tax inclusion settings (Tax Inclusive vs Tax Exclusive)

2. Booking System

  • Secure booking flow with date range selection
  • Guest checkout (auto-account creation)
  • Auto-fill guest details for authenticated users
  • Book for others (manual entry)
  • Dynamic occupancy selection with real-time validation
  • Service add-ons during checkout
  • Detailed price breakdown (base rate, extra guest charges, services, tax)

3. Payment Integration

  • Razorpay online payment (Test Mode)
  • Pay at Hotel option
  • Automatic payment verification

4. Email Notifications

  • Automated booking confirmation emails
  • Payment receipt notifications
  • Cancellation confirmations

5. Dynamic Pages (CMS)

  • Create and manage pages (Privacy Policy, Terms, etc.)
  • HTML content support
  • Status management (active/inactive)

6. Offline Guest Management

  • Client-side offline support
  • UUID-based sync for cross-device records
  • Conflict resolution for concurrent edits
  • localStorage persistence with sync queue

Docker Configuration

yaml
services:
  mariadb:
    image: mariadb:10.11
    container_name: hotel_mariadb
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MYSQL_DATABASE: ${DB_NAME}
      MYSQL_USER: ${DB_USER}
      MYSQL_PASSWORD: ${DB_PASSWORD}
    ports:
      - "3306:3306"
    volumes:
      - mariadb_data:/var/lib/mysql
    networks:
      - hotel_network

volumes:
  mariadb_data:

API Endpoints Overview

ModuleEndpoints
AuthPOST /api/auth/login, POST /api/auth/register
RoomsGET /api/rooms, POST /api/rooms, PUT /api/rooms/:id
BookingsGET /api/bookings, POST /api/bookings, PUT /api/bookings/:id
PaymentsPOST /api/razorpay/create-order, POST /api/razorpay/verify
ServicesGET /api/services, POST /api/services
WebhooksGET /api/webhooks, POST /api/webhooks
BrandingGET /api/branding, PUT /api/branding
iCalGET /api/ical/room/:id/export, POST /api/ical/sync

Build and Run

bash
# Start database
docker-compose up -d

# Setup backend
cd server
npm install
cp .env.example .env
# Edit .env with your configuration
node seed.js
npm start

# Setup frontend (in client directory)
cd client
npm install
npm run dev

Conclusion

Full-Stack Hotel Reservation System (FSHRS) represents a production-ready hospitality management platform with comprehensive features for modern hotel operations. The system demonstrates sophisticated engineering patterns including:

  • ACID-compliant database transactions with MariaDB InnoDB
  • Race condition prevention through inventory locking
  • Two-way OTA synchronization with external platforms
  • Flexible white-labeling for multi-tenant deployments
  • Webhook-based event system for external integrations
  • Role-based access control with granular permissions
  • Offline-first guest management capabilities

The architecture is designed for scalability and extensibility, with clean separation between frontend and backend concerns. The system is suitable for small to medium-sized hotels looking for a self-hosted reservation management solution.

(End of file - 685 lines)

Architecture Feedback

Spotted a potential optimization or antipattern? Let me know.

Submit a Technical Suggestion