Backend-agnostic scheduling adapter hub. Currently bridges Acuity Scheduling via Playwright browser automation, with architecture designed to support additional scheduling backends.
Formerly
acuity-middleware. Historical GitHub URLs may redirect, but the canonical repo isJesssullivan/scheduling-bridge.
An HTTP server wrapping Playwright wizard flows that automate the Acuity booking UI. The bridge uses Effect TS for resource lifecycle management (browser/page acquisition and release).
HTTP Request
-> server/handler.ts (route matching, auth, JSON serialization)
-> acuity-service-catalog.ts (static env catalog -> BUSINESS -> scraper fallback)
-> steps/ (Effect TS programs for each wizard stage)
-> browser-service.ts (Playwright lifecycle via Effect Layer)
-> selectors.ts (CSS selector registry with fallback chains)
- server/handler.ts -- Standalone Node.js HTTP server with Bearer token auth
- acuity-service-catalog.ts -- Shared service source order and cache for static config, BUSINESS extraction, and scraper fallback
- browser-service.ts -- Effect TS Layers for a warm shared browser process plus request-scoped page sessions
- acuity-wizard.ts -- Full
SchedulingAdapterimplementation (local Playwright or remote HTTP proxy) - remote-adapter.ts -- HTTP client adapter for proxying to a remote middleware instance
- selectors.ts -- Single source of truth for all Acuity DOM selectors
- steps/ -- Individual wizard step programs plus BUSINESS extraction helpers
- acuity-scraper.ts -- Deprecated read fallback for services, dates, and time slots
| Method | Path | Description |
|---|---|---|
| GET | /health |
Health check (no auth required) |
| GET | /services |
List appointment types via SERVICES_JSON -> BUSINESS -> scraper fallback |
| GET | /services/:id |
Get a specific service |
| POST | /availability/dates |
Available dates for a service |
| POST | /availability/slots |
Time slots for a specific date |
| POST | /availability/check |
Check if a slot is available |
| POST | /booking/create |
Create a booking (standard) |
| POST | /booking/create-with-payment |
Create booking with payment bypass (coupon) |
GET /health is the stable downstream runtime-truth surface.
In addition to basic runtime data, it now publishes:
- release tuple:
releaseShareleaseRefreleaseVersionreleaseBuiltAt- nested
release.{ sha, ref, version, builtAt, modalEnvironment }
- protocol tuple:
protocolVersion- nested
protocol.version protocol.flowOwner = "scheduling-bridge"protocol.backend = "acuity"protocol.transport = "http-json"protocol.endpointsprotocol.capabilities
Downstream apps should use this tuple to assert which bridge release and protocol surface they are talking to during beta validation and rollout claims.
This tuple is the supported runtime truth surface for adopters. Downstream apps
should not infer bridge ownership from package metadata, branch names, or Modal
dashboard state when /health is available.
| Variable | Required | Default | Description |
|---|---|---|---|
PORT |
No | 3001 |
Server port |
ACUITY_BASE_URL |
No | https://MassageIthaca.as.me |
Acuity scheduling page URL |
AUTH_TOKEN |
Recommended | -- | Bearer token for all endpoints (except /health) |
ACUITY_BYPASS_COUPON |
For payment bypass | -- | 100% gift certificate code |
PLAYWRIGHT_HEADLESS |
No | true |
Run browser headless |
PLAYWRIGHT_TIMEOUT |
No | 30000 |
Page operation timeout (ms) |
CHROMIUM_EXECUTABLE_PATH |
No | -- | Custom Chromium path (for Lambda/serverless) |
CHROMIUM_LAUNCH_ARGS |
No | -- | Comma-separated Chromium args |
SERVICES_JSON |
No | -- | Optional static service catalog to bypass live Acuity reads |
ACUITY_SERVICE_CACHE_TTL_MS |
No | 300000 |
TTL for cached live service catalogs before BUSINESS/scraper refresh |
SCHEDULING_BRIDGE_SLOT_PROFILE_THRESHOLD_MS |
No | 1500 |
Threshold in ms for logging long-tail slot-read profile events |
SCHEDULING_BRIDGE_PROFILE_SLOT_READS |
No | false |
Force logging of slot-read profile events even when under threshold |
MIDDLEWARE_RELEASE_SHA |
No | -- | Release commit SHA exposed via /health |
MIDDLEWARE_RELEASE_REF |
No | -- | Release ref/tag exposed via /health |
MIDDLEWARE_RELEASE_VERSION |
No | -- | Release version exposed via /health |
MIDDLEWARE_RELEASE_BUILT_AT |
No | -- | Build timestamp exposed via /health |
MIDDLEWARE_BUILD_TIMESTAMP |
No | -- | Legacy fallback build timestamp for /health |
The bridge emits NDJSON logs to stdout/stderr for runtime analysis.
/healthremains the authoritative runtime-truth surface for downstream apps- request handlers emit request-scoped structured events, including
requestId - long-tail slot reads emit
slot_read_profileevents with phase timings SCHEDULING_BRIDGE_PROFILE_SLOT_READS=1forces profile emission for all slot reads
pnpm install
pnpm dev # Development with tsx against src/server/handler.ts
# or
pnpm build && pnpm start # Materialize Bazel-derived dist/ and start itdocker build -t scheduling-bridge .
docker run -p 3001:3001 \
-e AUTH_TOKEN=your-secret-token \
-e ACUITY_BASE_URL=https://YourBusiness.as.me \
-e ACUITY_BYPASS_COUPON=your-coupon-code \
scheduling-bridge# Set secrets in Modal dashboard first:
# AUTH_TOKEN, ACUITY_BASE_URL, ACUITY_BYPASS_COUPON
# The Modal workflow materializes the Bazel-derived pkg/ before deploy.
modal deploy modal-app.pyThe supported deployment path for the live Acuity bridge is:
- merge to
main - let
.github/workflows/deploy-modal.ymldeploymodal-app.py - inject
MIDDLEWARE_RELEASE_SHA,MIDDLEWARE_RELEASE_REF,MIDDLEWARE_RELEASE_VERSION, andMIDDLEWARE_RELEASE_BUILT_AT - verify the resulting bridge tuple via
GET /health
Operationally, this means:
- Modal deployment is part of release truth, not a side channel
- the live bridge should be identified by the
/healthrelease + protocol tuple - downstream apps should validate the tuple they expect before making rollout claims
nix develop # Enter dev shell with Node.js + Playwright
pnpm install
pnpm devCurrent release authority:
- canonical repo:
Jesssullivan/scheduling-bridge - npm package:
@tummycrypt/scheduling-bridge - GitHub Packages mirror:
@jesssullivan/scheduling-bridge
The current publish + deploy shape is:
- release metadata declared once
- Bazel validates/builds the publishable artifact
- CI dry-runs the extracted Bazel package surface before release
- GitHub Actions publishes that extracted artifact
- GitHub Actions deploys the Modal runtime from
main - downstream apps consume the published package and verify the live runtime
tuple via
/health
This repo is the sole owner of Acuity automation concerns. App repos and shared packages may consume the bridge and assert its runtime tuple, but they should not duplicate bridge runtime ownership or release truth logic.
Package CI and publish currently use the shared js-bazel-package workflow with
runner_mode: shared and publish_mode: same_runner.
The concrete shared-runner labels come from repository Actions variables and must be proven by green workflow runs before they are treated as operational truth. Keep private runner topology and apply details out of this public repo.
pnpm install # Install dependencies
pnpm dev # Start dev server with tsx
pnpm typecheck # Run Bazel typecheck target
pnpm build # Materialize local pkg/ and dist/ from bazel-bin/pkg
pnpm test # Run Bazel test target
pnpm docs:generateMIT