Skip to main content

Scheduler Architecture

This document covers the full technical architecture of the Wrkbelt Scheduler, spanning CDN deployment, frontend application lifecycle, backend API services, and the caching/invalidation strategy.

High-Level Overview

The system has three major layers:

  1. CDN Layer — Static asset hosting (S3 + CloudFront) with versioned deployment
  2. Frontend Layer — React application rendered inside a Shadow DOM on the customer's website
  3. Backend Layer — NestJS API with vendor integrations, real-time socket communication, and session analytics

CDN Deployment Architecture

Codebase

All CDN infrastructure lives in apps/products/scheduler-cdn/:

apps/products/scheduler-cdn/
├── src/
│ ├── entry.ts # Lightweight bootstrap script (becomes scheduler.js)
│ ├── loader.ts # Shadow DOM mounting + React root creation
│ ├── constants.ts # CDN-side constants (asset paths, events)
│ └── styles/
│ └── main.css # Tailwind + CSS variables injected into Shadow DOM
├── infra/
│ ├── constants.ts # Infra constants (cache durations, domains, etc.)
│ ├── artifacts.ts # S3 upload logic with Cache-Control headers
│ ├── cache-policies.ts # CloudFront cache policy definitions
│ ├── router.ts # CloudFront distribution routes + edge functions
│ ├── bucket.ts # S3 bucket configuration
│ └── lambdas/
│ └── invalidate-cache.ts # On-demand CloudFront invalidation Lambda
└── sst.config.ts # SST entry point composing all infra

Build Output

Vite produces content-hashed bundles:

dist/
├── scheduler.js # Entry script (non-versioned, always fresh)
├── version-manifest.json # Points to current versioned entry chunk
├── build-info.json # Build metadata
├── loader.js # Loader utilities
└── {commit-hash}/
├── js/
│ ├── scheduler-core.{hash}.js # Main application chunk
│ └── vendor.{hash}.js # Third-party vendor chunk
└── css/
└── main.{hash}.css # Compiled styles

Loading Sequence

Key points:

  1. scheduler.js is the non-versioned entry point — always served fresh (no-cache, no-store, must-revalidate)
  2. version-manifest.json is fetched with a ?_={timestamp} cache-buster to ensure freshness
  3. The manifest returns the path to the current versioned scheduler-core chunk
  4. entry.ts appends ?v={buildTime} when loading the core chunk — this busts the browser cache per deploy while CloudFront strips query params (queryStringBehavior: 'none'), so edge caching is unaffected
  5. Versioned assets under {commit}/ are content-hashed and cached with public, max-age=604800 (1 week, with a graduation plan to 1 year)

Caching Strategy

The caching model has two independent layers:

CDN Edge (CloudFront Cache Policies)

PolicyApplied ToTTLQuery Strings
noCachePolicy/scheduler.js, /version-manifest.json, /loader.js, /build-info.json0s default, 1s maxAll (forwarded)
longCachePolicy/* (versioned assets)1 yearNone (stripped)

Cache policies are defined in infra/cache-policies.ts using CACHE_DURATIONS constants from infra/constants.ts.

Browser (S3 Cache-Control Headers)

File PatternCache-Control Header
Root-level files (no / in key)no-cache, no-store, must-revalidate
Versioned files ({commit}/**)public, max-age=604800

Headers are set via getCacheControl() in infra/artifacts.ts during S3 upload.

Cache-Busting Mechanism

Each deploy produces a unique buildTime in the version manifest. entry.ts appends ?v={buildTime} to the chunk URL. This creates a unique browser cache key per deploy, even if the underlying file hash hasn't changed (same commit, different build). CloudFront ignores the query param entirely.

Graduation plan (defined in CACHE_DURATIONS.VERSIONED):

  1. Current: 1 week (max-age=604800) — conservative baseline
  2. Next: 1 month (max-age=2592000) — after validating cache-busting in production
  3. Target: 1 year (max-age=31536000) + immutable directive

Deploy Invalidation

Every deploy triggers invalidation: { paths: ['/*'] } on the CloudFront distribution, clearing all edge-cached content. An additional Lambda (invalidate-cache.ts) supports on-demand invalidation of specific paths.

CloudFront Route Configuration

Defined in infra/router.ts:

RouteCache PolicyPurpose
/scheduler.jsnoCachePolicyEntry script — must always be fresh
/version-manifest.jsonnoCachePolicyDeployment manifest — must always be fresh
/loader.jsnoCachePolicyLoader utilities — must always be fresh
/build-info.jsonnoCachePolicyBuild metadata — must always be fresh
/*longCachePolicyAll versioned assets (catch-all)

Edge functions handle CORS: viewerRequest returns 204 for OPTIONS preflight, viewerResponse adds CORS headers to all responses.


Frontend Architecture

Shadow DOM Mounting

The scheduler renders inside a Shadow DOM to achieve complete style isolation from the host website.

loader.ts handles this setup:

  1. Creates or reuses #wrkbelt-scheduler-container
  2. Attaches a Shadow DOM (mode: 'open')
  3. Injects compiled CSS (Tailwind + CSS custom properties for theming) into the shadow root
  4. Creates a portal container inside the shadow root
  5. Mounts a React root on the portal container

The Dialog/Modal component uses a portalContainer prop to render inside the shadow root rather than document.body, ensuring overlays remain within the encapsulated scope.

React Component Tree

Provider hierarchy:

  • ApiKeyProvider — Makes the organization API key available via context
  • StoreProvider — Global state via useReducer (Redux-style reducers for booking and navigation state)
  • ServiceRegistryProvider — Initializes the singleton service registry on mount

Service Registry

A singleton registry providing dependency injection for all frontend services. Initialized once when ServiceRegistryProvider mounts.

ServiceResponsibility
DefaultSchedulerApiServiceHTTP client for scheduler API endpoints (capacity, customer, booking)
SocketConnectionServiceWebSocket connection for real-time event streaming
SessionRecorderServiceImplEvent queue, session lifecycle, analytics tracking
GraphServiceService selection graph traversal utilities
ServiceSelectionEngineProcesses answer selections, determines next state
SchedulerLifecycleServiceOrchestrates initialization, configuration fetch, modal state

State Management

Two reducers managed via useReducer in StoreProvider:

bookingReducer — Tracks booking data:

  • Selected service, timeslot, customer info, zip code
  • Service selection state (graph traversal position)

navigationReducer — Tracks UI navigation:

  • Current step, step sequence, completed steps
  • Step state cache (memento pattern for back-navigation)
  • Navigation direction (for animations)

Step Manager & Dynamic Rendering

StepManager dynamically renders the current step component based on the step type from the booking flow configuration:

  1. Reads currentStepType from the navigation store
  2. Looks up the component in StepsRegistry (a map of step type → React component)
  3. Generates typed props via getStepComponentProps() (step entity, saved state, onNext/onBack handlers)
  4. Renders the resolved component

Available step types: ZipCode, ServiceSelection, AdditionalDetails, Timeslot, Customer, Summary

Scheduler Lifecycle

SchedulerLifecycleService orchestrates the full initialization sequence:

  1. Apply default theme
  2. Fetch configuration from API (POST /booking-flow-routers/resolve with current page URL)
  3. Fetch button triggers (GET /booking-flow-routers/button-triggers)
  4. Apply configured theme
  5. Register global click listeners for booking flow triggers
  6. Initialize session recording when modal opens (create booking session, connect socket, start event queue processor)

Backend Architecture

API Layer (NestJS)

The scheduler backend is a set of NestJS modules within the API monolith:

ModuleKey Endpoints
BookingFlowRouterPOST /booking-flow-routers/resolve — URL-based flow resolution; GET /booking-flow-routers/button-triggers — DOM trigger configuration
SchedulerGET /scheduler/month-availability — Calendar data; GET /scheduler/date-timeslots — Available slots for a date
BookingPOST /bookings — Create booking in vendor system
BookingSessionSession recording endpoints, event ingestion via WebSocket

Vendor Strategy Pattern

The SchedulerService uses a strategy pattern to support multiple vendor FSM systems:

strategyFactory.getStrategy(vendorType) returns the appropriate strategy. Each strategy implements a common interface for:

  • getMonthAvailability() — Calendar-level availability
  • getDateTimeslots() — Specific timeslots for a date
  • createBooking() — Push confirmed booking to vendor

Real-Time Communication

Socket.IO handles bidirectional communication between the scheduler frontend and backend:

  • Session events — Step navigation, answer selections, timeslot views, errors
  • Booking session updates — Real-time session state synchronization
  • Event queue processing — Frontend queues events and flushes them in batches via socket, with retry logic for transient failures

Session Recording Pipeline

Events flow from UI interactions through an in-memory queue, are emitted via WebSocket, land in an SQS FIFO queue for ordered processing, and are persisted to MongoDB by a background worker.


Infrastructure Summary

ResourceServicePurpose
S3 BucketAsset storageHosts all scheduler static assets with versioned lifecycle policies
CloudFront DistributionCDNEdge caching, CORS, route-based cache policies
LambdaCache invalidationOn-demand CloudFront invalidation for emergency cache clearing
ECS (via Copilot)API hostingNestJS backend API
MongoDBDatabaseBooking sessions, flow configs, analytics events
SQS FIFOEvent queueOrdered processing of session recording events

Environment Domains

EnvironmentDomain
Locallocal.scheduler.wrkbelt.com
Previewpreview.scheduler.wrkbelt.com
Developmentdevelop.scheduler.wrkbelt.com
Stagingstaging.scheduler.wrkbelt.com
Productionscheduler.wrkbelt.com