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