Skip to content

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

LayerTechnologyPurpose
RuntimeBunFast JavaScript runtime, used instead of Node.js for speed
Backend FrameworkExpress 5.1HTTP routing, middleware pipeline, error handling
DatabaseFirebase FirestoreNoSQL document database, real-time capable
CacheUpstash RedisServerless Redis for caching and rate limiting
AuthenticationPASETO V4 (local)Stateless, encrypted tokens — safer alternative to JWT
Real-timeSocket.IO 4.8WebSocket-based bidirectional communication
File StorageAzure Blob StorageGuest documents, logos, attachments
FrontendNext.js 15 + React 19Server-side rendering, app router
UI Componentsshadcn/ui + Tailwind CSSAccessible, composable component library
ValidationZod (frontend), express-validator (backend)Schema-based input validation

Request Lifecycle

Every API request passes through a deterministic middleware pipeline before reaching the controller:

mermaid
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
    end

Middleware Pipeline (in order)

  1. Rate Limiter — Prevents abuse. Configurable per-route limits using Redis-backed sliding windows.
  2. Request Tracker — Logs incoming requests for analytics and debugging.
  3. Auth Middleware — Decrypts the PASETO V4 token from the Authorization header, validates expiry, and attaches the decoded user payload to req.user.
  4. RBAC Middleware — Reads req.user.role and checks the requested action against the role's permission set. Returns 403 if unauthorized.
  5. Validation — Route-specific validators (express-validator) sanitize and validate the request body/params.
  6. 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

mermaid
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 dashboard

Token Refresh Flow

mermaid
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
    end

Token Details

TokenTypeTTLStoragePurpose
Access TokenPASETO V4 local15 minutesIn-memory (frontend)API authentication
Refresh TokenPASETO V4 local7 dayshttpOnly cookieSilent token renewal

Access token payload:

json
{
  "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

mermaid
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 aggregates

TTL Strategy

Data TypeTTLRationale
Room list10 minutesRooms change infrequently
Room categories10 minutesRarely modified
Dashboard stats5 minutesBalance freshness vs. cost
Single booking3 minutesBookings update more often
User profile15 minutesStable data

Cache Invalidation

On every write operation (create, update, delete), the controller invalidates all related cache keys:

js
// 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:

js
// Frontend connection
const socket = io(BACKEND_URL, {
  auth: { token: accessToken }
});

// Server-side: user joins org room after auth
socket.join(`org:${orgId}`);

Event Types

EventDirectionPayloadTrigger
booking:createdServer → ClientBooking objectNew booking created
booking:updatedServer → ClientUpdated bookingStatus change, room change
booking:checkinServer → ClientBooking + roomGuest checked in
booking:checkoutServer → ClientBooking + invoiceGuest checked out
room:statusChangedServer → ClientRoom objectAvailability toggled
notification:newServer → ClientNotification objectAny notification trigger
dashboard:refreshServer → Client— (signal only)Data changed, re-fetch

Broadcasting Pattern

Events are always scoped to the organization room:

js
// 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

mermaid
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

CollectionDescriptionKey Fields
organizationsOrg config, branding, settingsname, address, logo, gstNumber
usersUser accounts, linked to orgsemail, name, orgId, roleId
rolesCustom role definitionsname, permissions[], orgId
roomsIndividual room recordsroomNumber, categoryId, isAvailable, orgId
room-categoriesRoom type groupingsname, basePrice, amenities[], orgId
bookingsBooking recordsguestId, rooms[], checkIn, checkOut, status, orgId
guestsGuest profilesname, email, phone, idProof, orgId
invoicesAuto-generated invoicesbookingId, items[], total, status, orgId
paymentsPayment recordsinvoiceId, amount, method, orgId
expensesExpense trackingvendorId, amount, category, orgId
vendorsVendor directoryname, gstNumber, orgId
taxesTax configurations (GST slabs)name, rate, type, orgId
credit-notesInvoice adjustmentsinvoiceId, amount, reason, orgId
quotesBooking estimates / proformaguestId, rooms[], total, orgId
chartered_accountsChart of accountsaccountName, type, balance, orgId
notificationsIn-app notificationsuserId, message, read, orgId
audit_logsAction audit trailuserId, action, entity, timestamp, orgId
emailTemplatesCustomizable email templatestype, subject, body, orgId
seriesAuto-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:

js
// 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:

CodeMeaning
200Success
201Resource created
400Validation error / bad request
401Not authenticated
403Not authorized (RBAC)
404Resource not found
409Conflict (e.g., double-booking)
429Rate limited
500Server error

Released under the MIT License.