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:
- CDN Layer — Static asset hosting (S3 + CloudFront) with versioned deployment
- Frontend Layer — React application rendered inside a Shadow DOM on the customer's website
- 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:
scheduler.jsis the non-versioned entry point — always served fresh (no-cache, no-store, must-revalidate)version-manifest.jsonis fetched with a?_={timestamp}cache-buster to ensure freshness- The manifest returns the path to the current versioned
scheduler-corechunk entry.tsappends?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- Versioned assets under
{commit}/are content-hashed and cached withpublic, 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)
| Policy | Applied To | TTL | Query Strings |
|---|---|---|---|
noCachePolicy | /scheduler.js, /version-manifest.json, /loader.js, /build-info.json | 0s default, 1s max | All (forwarded) |
longCachePolicy | /* (versioned assets) | 1 year | None (stripped) |
Cache policies are defined in infra/cache-policies.ts using CACHE_DURATIONS constants from infra/constants.ts.
Browser (S3 Cache-Control Headers)
| File Pattern | Cache-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):
- Current: 1 week (
max-age=604800) — conservative baseline - Next: 1 month (
max-age=2592000) — after validating cache-busting in production - Target: 1 year (
max-age=31536000) +immutabledirective
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:
| Route | Cache Policy | Purpose |
|---|---|---|
/scheduler.js | noCachePolicy | Entry script — must always be fresh |
/version-manifest.json | noCachePolicy | Deployment manifest — must always be fresh |
/loader.js | noCachePolicy | Loader utilities — must always be fresh |
/build-info.json | noCachePolicy | Build metadata — must always be fresh |
/* | longCachePolicy | All 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:
- Creates or reuses
#wrkbelt-scheduler-container - Attaches a Shadow DOM (
mode: 'open') - Injects compiled CSS (Tailwind + CSS custom properties for theming) into the shadow root
- Creates a portal container inside the shadow root
- 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 contextStoreProvider— Global state viauseReducer(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.
| Service | Responsibility |
|---|---|
DefaultSchedulerApiService | HTTP client for scheduler API endpoints (capacity, customer, booking) |
SocketConnectionService | WebSocket connection for real-time event streaming |
SessionRecorderServiceImpl | Event queue, session lifecycle, analytics tracking |
GraphService | Service selection graph traversal utilities |
ServiceSelectionEngine | Processes answer selections, determines next state |
SchedulerLifecycleService | Orchestrates 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:
- Reads
currentStepTypefrom the navigation store - Looks up the component in
StepsRegistry(a map of step type → React component) - Generates typed props via
getStepComponentProps()(step entity, saved state, onNext/onBack handlers) - Renders the resolved component
Available step types: ZipCode, ServiceSelection, AdditionalDetails, Timeslot, Customer, Summary
Scheduler Lifecycle
SchedulerLifecycleService orchestrates the full initialization sequence:
- Apply default theme
- Fetch configuration from API (
POST /booking-flow-routers/resolvewith current page URL) - Fetch button triggers (
GET /booking-flow-routers/button-triggers) - Apply configured theme
- Register global click listeners for booking flow triggers
- 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:
| Module | Key Endpoints |
|---|---|
| BookingFlowRouter | POST /booking-flow-routers/resolve — URL-based flow resolution; GET /booking-flow-routers/button-triggers — DOM trigger configuration |
| Scheduler | GET /scheduler/month-availability — Calendar data; GET /scheduler/date-timeslots — Available slots for a date |
| Booking | POST /bookings — Create booking in vendor system |
| BookingSession | Session 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 availabilitygetDateTimeslots()— Specific timeslots for a datecreateBooking()— 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
| Resource | Service | Purpose |
|---|---|---|
| S3 Bucket | Asset storage | Hosts all scheduler static assets with versioned lifecycle policies |
| CloudFront Distribution | CDN | Edge caching, CORS, route-based cache policies |
| Lambda | Cache invalidation | On-demand CloudFront invalidation for emergency cache clearing |
| ECS (via Copilot) | API hosting | NestJS backend API |
| MongoDB | Database | Booking sessions, flow configs, analytics events |
| SQS FIFO | Event queue | Ordered processing of session recording events |
Environment Domains
| Environment | Domain |
|---|---|
| Local | local.scheduler.wrkbelt.com |
| Preview | preview.scheduler.wrkbelt.com |
| Development | develop.scheduler.wrkbelt.com |
| Staging | staging.scheduler.wrkbelt.com |
| Production | scheduler.wrkbelt.com |