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:
sequenceDiagram
participant C as Client
participant E as Express Router
participant RL as Rate Limiter
participant A as Auth Middleware
participant R as RBAC Middleware
participant CT as Controller
participant FS as Firestore
participant RD as Redis
participant S as Socket.IO
C->>E: HTTP Request
E->>RL: Check rate limit
RL->>A: Extract & verify PASETO token
A->>R: Check user role permissions
R->>CT: Execute business logic
alt Cache Hit
CT->>RD: Check Redis cache
RD-->>CT: Return cached data
else Cache Miss
CT->>FS: Query Firestore
FS-->>CT: Return documents
CT->>RD: Store in cache (with TTL)
end
CT-->>C: HTTP Response
opt Real-time Update
CT->>S: Emit event to org room
S-->>C: Push update via WebSocket
endMiddleware 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
sequenceDiagram
participant U as User
participant F as Frontend
participant B as Backend
participant DB as Firestore
participant C as Cookie Store
U->>F: Enter email + password
F->>B: POST /api/auth/login
B->>DB: Fetch user by email
DB-->>B: User document
B->>B: Verify password (bcrypt)
B->>B: Generate PASETO access token (15 min TTL)
B->>B: Generate PASETO refresh token (7 day TTL)
B-->>F: { accessToken } + Set-Cookie: refreshToken (httpOnly)
F->>C: Store refreshToken cookie
F->>F: Store accessToken in memory
U->>F: Sees dashboardToken Refresh Flow
sequenceDiagram
participant F as Frontend
participant B as Backend
participant C as Cookie Store
F->>B: POST /api/auth/refresh (cookie: refreshToken)
B->>B: Decrypt & validate refresh token
alt Valid
B->>B: Generate new access token
B->>B: Rotate refresh token
B-->>F: { accessToken } + Set-Cookie: new refreshToken
else Expired/Invalid
B-->>F: 401 Unauthorized
F->>F: Redirect to login
endToken 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
graph TD
REQ[Incoming Request] --> CHECK{Redis Cache?}
CHECK -->|Hit| RETURN[Return cached data]
CHECK -->|Miss| QUERY[Query Firestore]
QUERY --> STORE[Store in Redis with TTL]
STORE --> RETURN
WRITE[Write Operation] --> INVALIDATE[Invalidate related cache keys]
INVALIDATE --> FIRESTORE[Write to Firestore]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
erDiagram
organizations ||--o{ users : "has members"
organizations ||--o{ roles : "defines"
organizations ||--o{ rooms : "contains"
organizations ||--o{ room-categories : "groups"
organizations ||--o{ bookings : "tracks"
organizations ||--o{ guests : "registers"
organizations ||--o{ invoices : "generates"
organizations ||--o{ payments : "records"
organizations ||--o{ expenses : "logs"
organizations ||--o{ vendors : "manages"
organizations ||--o{ taxes : "configures"
organizations ||--o{ series : "numbers"
organizations ||--o{ notifications : "sends"
organizations ||--o{ audit_logs : "audits"
organizations ||--o{ emailTemplates : "customizes"
bookings ||--o{ invoices : "produces"
bookings }o--|| guests : "for"
bookings }o--o{ rooms : "reserves"
invoices ||--o{ payments : "settled by"
invoices ||--o{ credit-notes : "adjusted by"
rooms }o--|| room-categories : "belongs to"
expenses }o--o| vendors : "paid to"
organizations ||--o{ chartered_accounts : "accounting"
organizations ||--o{ quotes : "estimates"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 |