Architecture
This page provides a deep technical overview of the Booking Management System's architecture, covering the tech stack, request lifecycle, authentication, caching, real-time events, and the Firestore data model.
Tech Stack
| Layer | Technology | Purpose |
|---|---|---|
| Runtime | Bun | Fast JavaScript runtime, used instead of Node.js for speed |
| Backend Framework | Express 5.1 | HTTP routing, middleware pipeline, error handling |
| Database | Firebase Firestore | NoSQL document database, real-time capable |
| Cache | Upstash Redis | Serverless Redis for caching and rate limiting |
| Authentication | PASETO V4 (local) | Stateless, encrypted tokens — safer alternative to JWT |
| Real-time | Socket.IO 4.8 | WebSocket-based bidirectional communication |
| File Storage | Azure Blob Storage | Guest documents, logos, attachments |
| Frontend | Next.js 15 + React 19 | Server-side rendering, app router |
| UI Components | shadcn/ui + Tailwind CSS | Accessible, composable component library |
| Validation | Zod (frontend), express-validator (backend) | Schema-based input validation |
Request Lifecycle
Every API request passes through a deterministic middleware pipeline before reaching the controller:
Middleware Pipeline (in order)
- Rate Limiter — Prevents abuse. Configurable per-route limits using Redis-backed sliding windows.
- Request Tracker — Logs incoming requests for analytics and debugging.
- Auth Middleware — Decrypts the PASETO V4 token from the
Authorizationheader, validates expiry, and attaches the decoded user payload toreq.user. - RBAC Middleware — Reads
req.user.roleand checks the requested action against the role's permission set. Returns403if unauthorized. - Validation — Route-specific validators (express-validator) sanitize and validate the request body/params.
- Controller — Executes the business logic, interacts with Firestore/Redis, and returns the response.
Authentication Flow
BMS uses PASETO V4 (local encryption) for tokens instead of JWT. PASETO tokens are encrypted (not just signed), preventing token inspection by clients and eliminating entire classes of JWT vulnerabilities.
Login Flow
Token Refresh Flow
Token Details
| Token | Type | TTL | Storage | Purpose |
|---|---|---|---|---|
| Access Token | PASETO V4 local | 15 minutes | In-memory (frontend) | API authentication |
| Refresh Token | PASETO V4 local | 7 days | httpOnly cookie | Silent token renewal |
Access token payload:
{
"userId": "abc123",
"email": "user@hotel.com",
"orgId": "org_456",
"role": "front-desk",
"permissions": ["booking:create", "booking:read", "room:read"],
"iat": "2026-02-23T10:00:00Z",
"exp": "2026-02-23T10:15:00Z"
}Caching Strategy
BMS uses Upstash Redis as a read-through cache to reduce Firestore reads and improve response latency.
Cache Architecture
Cache Key Convention
All cache keys follow the pattern:
{orgId}:{entity}:{identifier}Examples:
org_456:rooms:all → All rooms for org
org_456:room-categories:all → All categories for org
org_456:bookings:bk_789 → Single booking
org_456:dashboard:stats → Dashboard aggregatesTTL Strategy
| Data Type | TTL | Rationale |
|---|---|---|
| Room list | 10 minutes | Rooms change infrequently |
| Room categories | 10 minutes | Rarely modified |
| Dashboard stats | 5 minutes | Balance freshness vs. cost |
| Single booking | 3 minutes | Bookings update more often |
| User profile | 15 minutes | Stable data |
Cache Invalidation
On every write operation (create, update, delete), the controller invalidates all related cache keys:
// Example: After creating a booking
await redis.del(`${orgId}:bookings:all`);
await redis.del(`${orgId}:rooms:availability`);
await redis.del(`${orgId}:dashboard:stats`);This ensures subsequent reads always fetch fresh data from Firestore and re-populate the cache.
Real-Time Events (Socket.IO)
Socket.IO provides instant updates to all connected clients within an organization.
Connection Setup
When a user authenticates, the frontend connects to Socket.IO and joins the organization's room:
// Frontend connection
const socket = io(BACKEND_URL, {
auth: { token: accessToken }
});
// Server-side: user joins org room after auth
socket.join(`org:${orgId}`);Event Types
| Event | Direction | Payload | Trigger |
|---|---|---|---|
booking:created | Server → Client | Booking object | New booking created |
booking:updated | Server → Client | Updated booking | Status change, room change |
booking:checkin | Server → Client | Booking + room | Guest checked in |
booking:checkout | Server → Client | Booking + invoice | Guest checked out |
room:statusChanged | Server → Client | Room object | Availability toggled |
notification:new | Server → Client | Notification object | Any notification trigger |
dashboard:refresh | Server → Client | — (signal only) | Data changed, re-fetch |
Broadcasting Pattern
Events are always scoped to the organization room:
// Server-side emission
io.to(`org:${orgId}`).emit('booking:created', {
booking: newBooking,
timestamp: new Date().toISOString()
});Firestore Data Model
All collections live at the top level in Firestore. Every document contains an orgId field for multi-tenant isolation.
Collection Overview
Key Collections
| Collection | Description | Key Fields |
|---|---|---|
organizations | Org config, branding, settings | name, address, logo, gstNumber |
users | User accounts, linked to orgs | email, name, orgId, roleId |
roles | Custom role definitions | name, permissions[], orgId |
rooms | Individual room records | roomNumber, categoryId, isAvailable, orgId |
room-categories | Room type groupings | name, basePrice, amenities[], orgId |
bookings | Booking records | guestId, rooms[], checkIn, checkOut, status, orgId |
guests | Guest profiles | name, email, phone, idProof, orgId |
invoices | Auto-generated invoices | bookingId, items[], total, status, orgId |
payments | Payment records | invoiceId, amount, method, orgId |
expenses | Expense tracking | vendorId, amount, category, orgId |
vendors | Vendor directory | name, gstNumber, orgId |
taxes | Tax configurations (GST slabs) | name, rate, type, orgId |
credit-notes | Invoice adjustments | invoiceId, amount, reason, orgId |
quotes | Booking estimates / proforma | guestId, rooms[], total, orgId |
chartered_accounts | Chart of accounts | accountName, type, balance, orgId |
notifications | In-app notifications | userId, message, read, orgId |
audit_logs | Action audit trail | userId, action, entity, timestamp, orgId |
emailTemplates | Customizable email templates | type, subject, body, orgId |
series | Auto-increment series (invoice #, booking #) | prefix, current, orgId |
Document ID Strategy
- Most documents use Firestore's auto-generated IDs.
- Series documents use deterministic IDs (
{orgId}_{seriesType}) for atomic increments. - User documents use the Firebase Auth UID as the document ID.
Error Handling
BMS uses a centralized error handler middleware:
// All controllers throw structured errors
throw { status: 404, message: 'Booking not found', code: 'BOOKING_NOT_FOUND' };
// Caught by errorHandler middleware → consistent JSON response
{
"success": false,
"error": {
"code": "BOOKING_NOT_FOUND",
"message": "Booking not found"
}
}HTTP status codes follow REST conventions:
| Code | Meaning |
|---|---|
200 | Success |
201 | Resource created |
400 | Validation error / bad request |
401 | Not authenticated |
403 | Not authorized (RBAC) |
404 | Resource not found |
409 | Conflict (e.g., double-booking) |
429 | Rate limited |
500 | Server error |