Technology Stack
| Layer | Technology | Version/Details |
|---|---|---|
| Frontend | Next.js | 14 (App Router) |
| UI Framework | Tailwind CSS + Shadcn UI | - |
| Backend | Node.js + Express | ^4.18.2 |
| Database | MariaDB | via Docker |
| ORM | Sequelize | ^6.35.1 |
| Authentication | JWT + bcryptjs | - |
| Payment | Razorpay | ^2.9.6 |
| Nodemailer | ^7.0.10 | |
| Scheduler | node-cron | ^3.0.3 |
| iCal | node-ical + ical-generator | - |
| Docker | Docker 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
- ACID Database Transactions: Using MariaDB InnoDB engine ensures reliable booking operations with row-level locking to prevent double-bookings.
- 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.
- White-Label Ready: Complete branding customization enables multi-tenant deployments where different hotel brands can use the same codebase with distinct visual identities.
- OTA Synchronization: Native support for iCal format enables two-way synchronization with external platforms like Airbnb and Booking.com.
- 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:
-- 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)
// 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:
// 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:
// 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
| Role | Permissions |
|---|---|
| Guest | Browse rooms, make bookings, manage profile, view booking history |
| Admin | All guest permissions + manage bookings, rooms, customers, services, basic settings |
| Developer | All admin permissions + white-labeling, email/payment config, API/webhook management, factory reset |
Authentication Flow
// 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
// 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
// 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
// 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)
// 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)
// 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
// 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
// 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
// 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
// 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
// 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
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
| Module | Endpoints |
|---|---|
| Auth | POST /api/auth/login, POST /api/auth/register |
| Rooms | GET /api/rooms, POST /api/rooms, PUT /api/rooms/:id |
| Bookings | GET /api/bookings, POST /api/bookings, PUT /api/bookings/:id |
| Payments | POST /api/razorpay/create-order, POST /api/razorpay/verify |
| Services | GET /api/services, POST /api/services |
| Webhooks | GET /api/webhooks, POST /api/webhooks |
| Branding | GET /api/branding, PUT /api/branding |
| iCal | GET /api/ical/room/:id/export, POST /api/ical/sync |
Build and Run
# 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 devConclusion
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.