@tummycrypt/acuity-middleware 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/build-paper.yml +39 -0
- package/.github/workflows/ci.yml +37 -0
- package/Dockerfile +53 -0
- package/README.md +103 -0
- package/docs/blog-post.mdx +240 -0
- package/docs/paper/IEEEtran.bst +2409 -0
- package/docs/paper/IEEEtran.cls +6347 -0
- package/docs/paper/acuity-middleware-paper.tex +375 -0
- package/docs/paper/balance.sty +87 -0
- package/docs/paper/references.bib +231 -0
- package/docs/paper.md +400 -0
- package/flake.nix +32 -0
- package/modal-app.py +82 -0
- package/package.json +48 -0
- package/src/adapters/acuity-scraper.ts +543 -0
- package/src/adapters/types.ts +193 -0
- package/src/core/types.ts +325 -0
- package/src/index.ts +75 -0
- package/src/middleware/acuity-wizard.ts +456 -0
- package/src/middleware/browser-service.ts +183 -0
- package/src/middleware/errors.ts +70 -0
- package/src/middleware/index.ts +80 -0
- package/src/middleware/remote-adapter.ts +246 -0
- package/src/middleware/selectors.ts +308 -0
- package/src/middleware/server.ts +372 -0
- package/src/middleware/steps/bypass-payment.ts +226 -0
- package/src/middleware/steps/extract.ts +174 -0
- package/src/middleware/steps/fill-form.ts +359 -0
- package/src/middleware/steps/index.ts +27 -0
- package/src/middleware/steps/navigate.ts +537 -0
- package/src/middleware/steps/read-availability.ts +399 -0
- package/src/middleware/steps/read-slots.ts +405 -0
- package/src/middleware/steps/submit.ts +168 -0
- package/src/server.ts +5 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
\documentclass[conference]{IEEEtran}
|
|
2
|
+
|
|
3
|
+
\usepackage[utf8]{inputenc}
|
|
4
|
+
\usepackage[T1]{fontenc}
|
|
5
|
+
\usepackage{amsmath,amssymb}
|
|
6
|
+
\usepackage{graphicx}
|
|
7
|
+
\usepackage{xcolor}
|
|
8
|
+
\usepackage{listings}
|
|
9
|
+
\usepackage{booktabs}
|
|
10
|
+
\usepackage{array}
|
|
11
|
+
\usepackage{hyperref}
|
|
12
|
+
\usepackage{balance}
|
|
13
|
+
\usepackage{url}
|
|
14
|
+
\usepackage{cite}
|
|
15
|
+
|
|
16
|
+
% Code listing colors
|
|
17
|
+
\definecolor{codebg}{HTML}{F8F8F8}
|
|
18
|
+
\definecolor{tskw}{HTML}{7C3AED}
|
|
19
|
+
\definecolor{tsstr}{HTML}{059669}
|
|
20
|
+
\definecolor{tscmt}{HTML}{6B7280}
|
|
21
|
+
\definecolor{tstype}{HTML}{2563EB}
|
|
22
|
+
|
|
23
|
+
% TypeScript language definition
|
|
24
|
+
\lstdefinelanguage{TypeScript}{
|
|
25
|
+
keywords={function, return, const, let, if, else, switch, case, class, interface, type, extends, implements, import, export, from, as, new, this, async, await, yield, default, typeof, void, null, undefined, true, false, in, of},
|
|
26
|
+
keywordstyle=\color{tskw}\bfseries,
|
|
27
|
+
ndkeywords={string, number, boolean, Promise, Effect, Either, TaskEither, SchedulingResult, SchedulingError, SchedulingAdapter, Service, Booking, BookingRequest, Provider, TimeSlot, AvailableDate, SlotReservation, ClientInfo, Layer, Context, Exit, Cause, BrowserService},
|
|
28
|
+
ndkeywordstyle=\color{tstype},
|
|
29
|
+
sensitive=true,
|
|
30
|
+
comment=[l]{//},
|
|
31
|
+
morecomment=[s]{/*}{*/},
|
|
32
|
+
commentstyle=\color{tscmt}\itshape,
|
|
33
|
+
string=[b]',
|
|
34
|
+
morestring=[b]",
|
|
35
|
+
morestring=[b]`,
|
|
36
|
+
stringstyle=\color{tsstr},
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
% Python language additions
|
|
40
|
+
\lstdefinelanguage{ModalPython}{
|
|
41
|
+
language=Python,
|
|
42
|
+
morekeywords={modal, Image, app, function, web_server, concurrent},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
\lstset{
|
|
46
|
+
basicstyle=\ttfamily\footnotesize,
|
|
47
|
+
backgroundcolor=\color{codebg},
|
|
48
|
+
frame=single,
|
|
49
|
+
framerule=0.4pt,
|
|
50
|
+
breaklines=true,
|
|
51
|
+
breakatwhitespace=true,
|
|
52
|
+
tabsize=2,
|
|
53
|
+
showstringspaces=false,
|
|
54
|
+
numbers=left,
|
|
55
|
+
numberstyle=\tiny\color{tscmt},
|
|
56
|
+
numbersep=5pt,
|
|
57
|
+
xleftmargin=1.5em,
|
|
58
|
+
aboveskip=0.5\baselineskip,
|
|
59
|
+
belowskip=0.5\baselineskip,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
\begin{document}
|
|
63
|
+
|
|
64
|
+
\title{Browser Automation Middleware for Zero-Downtime SaaS Migration: A Scheduling System Case Study}
|
|
65
|
+
|
|
66
|
+
\author{\IEEEauthorblockN{Jess Sullivan}
|
|
67
|
+
\IEEEauthorblockA{Tinyland Inc.\\
|
|
68
|
+
jess@tinyland.dev}}
|
|
69
|
+
|
|
70
|
+
\maketitle
|
|
71
|
+
|
|
72
|
+
\begin{abstract}
|
|
73
|
+
Small-business SaaS platforms create vendor lock-in through API paywalls and proprietary data formats. When the vendor restricts programmatic access to premium pricing tiers, businesses face a choice between paying for API access they should already have or accepting that their own data is held hostage. We present a browser automation middleware architecture that implements a standardized 16-method scheduling adapter interface by puppeteering the vendor's public-facing web UI via headless Playwright, deployed as a containerized service on Modal Labs. A feature-flag-driven backend selector enables zero-downtime migration from the legacy vendor to a homegrown PostgreSQL backend, implementing the strangler fig pattern against a third-party SaaS dependency. The system has been deployed in production since March 2026, processing real appointment bookings across both backends simultaneously. We report on the architecture, the dual functional programming approach (Effect TS for browser lifecycle management, fp-ts for adapter composition), the reliability challenges of DOM automation against a React SPA with Emotion CSS, and the lessons learned from automating 604 legacy appointments across 62 weeks of calendar data. The middleware replaces 8 blocked REST API endpoints through DOM interaction alone, demonstrating that browser automation is a viable---if intentionally temporary---bridge pattern for escaping SaaS vendor lock-in.
|
|
74
|
+
\end{abstract}
|
|
75
|
+
|
|
76
|
+
\begin{IEEEkeywords}
|
|
77
|
+
browser automation, middleware, SaaS migration, strangler fig pattern, scheduling systems, vendor lock-in, adapter pattern, headless browser
|
|
78
|
+
\end{IEEEkeywords}
|
|
79
|
+
|
|
80
|
+
%% ============================================================
|
|
81
|
+
\section{Introduction}
|
|
82
|
+
|
|
83
|
+
The modern small business operates on a stack of vertical SaaS products: scheduling, payments, email marketing, inventory, point-of-sale. Each product captures business data in proprietary formats behind vendor-controlled APIs. When the relationship sours---pricing changes, feature regression, acquisition, or simple neglect---the business discovers that migration is not a product feature. It is an engineering problem, and one the vendor has no incentive to solve.
|
|
84
|
+
|
|
85
|
+
Acuity Scheduling, a Squarespace subsidiary, exemplifies this dynamic. The platform provides appointment booking for service businesses (massage therapy, consulting, tutoring) via an embeddable iframe widget and a web-based admin panel. The REST API that would enable programmatic access to appointment data, availability, and booking operations is gated behind the ``Powerhouse'' plan---a pricing tier that returns HTTP 403 on all endpoints for lower-tier accounts~\cite{acuity-api}. The business's own appointment history, client records, and scheduling configuration are accessible only through the web UI.
|
|
86
|
+
|
|
87
|
+
This paper presents a middleware architecture that solves the migration problem through browser automation. Rather than reverse-engineering the vendor's internal APIs or negotiating for API access, the system drives the vendor's public-facing booking wizard via headless Playwright~\cite{playwright}, translating standardized adapter method calls into sequences of DOM interactions. The middleware is deployed as a containerized service on Modal Labs~\cite{modal}, providing serverless scaling with warm container pools for acceptable latency.
|
|
88
|
+
|
|
89
|
+
The key insight is that browser automation middleware is not the destination---it is the bridge. The architecture implements the strangler fig pattern~\cite{fowler-strangler} with a feature-flag-controlled backend selector that routes scheduling operations to either the legacy vendor (via browser automation) or a homegrown PostgreSQL backend (via direct queries). As the homegrown backend reaches feature parity, the browser middleware layer becomes disposable. The adapter interface ensures that consumers of scheduling operations are completely isolated from which backend serves them.
|
|
90
|
+
|
|
91
|
+
\subsection{Contributions}
|
|
92
|
+
|
|
93
|
+
This paper makes four contributions:
|
|
94
|
+
|
|
95
|
+
\begin{enumerate}
|
|
96
|
+
\item A formal 16-method \texttt{SchedulingAdapter} interface that abstracts scheduling operations (services, availability, reservations, bookings, clients) across heterogeneous backends, with all methods returning monadic \texttt{TaskEither<SchedulingError, T>} for composable error handling.
|
|
97
|
+
|
|
98
|
+
\item An Effect TS middleware layer that implements this interface via headless browser automation, using typed effect programs for each wizard step, managed browser lifecycle via \texttt{acquireRelease}, and a CSS selector registry with fallback chains for resilience against DOM instability.
|
|
99
|
+
|
|
100
|
+
\item A feature-flag-driven backend selection mechanism with hostname override, enabling both backends to serve production traffic simultaneously with instant rollback capability.
|
|
101
|
+
|
|
102
|
+
\item Production deployment evidence from a real massage therapy practice, including reliability data from 604 automated legacy appointment operations and concurrent dual-backend operation.
|
|
103
|
+
\end{enumerate}
|
|
104
|
+
|
|
105
|
+
%% ============================================================
|
|
106
|
+
\section{Related Work}
|
|
107
|
+
|
|
108
|
+
\subsection{Legacy System Modernization}
|
|
109
|
+
|
|
110
|
+
The Carnegie Mellon Software Engineering Institute taxonomy~\cite{sei-legacy} classifies modernization strategies along a spectrum from wrapping (black-box, no source access) to reengineering (white-box, full source transformation). Browser automation middleware is a form of black-box wrapping---the legacy system's UI is the only interface analyzed. Seacord et al.~\cite{seacord-modernizing} formalize the risk-managed approach to modernization, emphasizing incremental strategies that preserve system availability during transition. Our feature-flag approach implements their recommended ``parallel operation'' phase.
|
|
111
|
+
|
|
112
|
+
\subsection{Wrapper-Based Evolution}
|
|
113
|
+
|
|
114
|
+
Sneed~\cite{sneed-wrapping} proposes wrapping legacy information systems with service-oriented interfaces, treating the wrapper as a translation layer between modern consumers and legacy implementations. Rahgozar and Oroumchian~\cite{rahgozar-patterns} identify three design patterns for wrapper interfaces: Lowest Common Denominator, Most Popular, and Negotiated. Our \texttt{SchedulingAdapter} interface follows the Negotiated pattern---the 16 methods represent the intersection of capabilities needed by the application, not the full surface area of any single backend.
|
|
115
|
+
|
|
116
|
+
Prior wrapper work focused on mainframe terminal interfaces (3270 screen scraping) and COBOL API wrapping~\cite{sei-legacy}. This work wraps a modern React single-page application rendered with Emotion CSS-in-JS---a qualitatively different challenge where DOM structure is dynamic, CSS class names are hash-unstable, and user interactions trigger asynchronous client-side state transitions rather than synchronous server round-trips.
|
|
117
|
+
|
|
118
|
+
\subsection{The Strangler Fig Pattern}
|
|
119
|
+
|
|
120
|
+
Fowler~\cite{fowler-strangler} describes the strangler fig pattern as a metaphor for incremental system replacement: new functionality is built alongside the old system, with a routing layer that progressively shifts traffic from legacy to replacement. Newman~\cite{newman-microservices} applies the pattern to microservice migration. Our \texttt{resolveBackend()} function is the routing facade, checking environment variables and hostname to select between the Acuity wizard adapter (legacy) and the homegrown PostgreSQL adapter (replacement).
|
|
121
|
+
|
|
122
|
+
\subsection{Anti-Corruption Layer}
|
|
123
|
+
|
|
124
|
+
Evans~\cite{evans-ddd} defines the Anti-Corruption Layer as a pattern for isolating a bounded context from the domain model of an external system. The \texttt{SchedulingAdapter} interface serves this role: it prevents Acuity's domain concepts (appointment type IDs, React wizard steps, Square payment integration) from leaking into the homegrown domain model (UUID-based services, slot reservations, Stripe/Venmo payments). The dual-ID resolution pattern in the homegrown adapter---accepting both UUID and legacy \texttt{acuityId}---is an explicit corruption-containment mechanism.
|
|
125
|
+
|
|
126
|
+
\subsection{Robotic Process Automation}
|
|
127
|
+
|
|
128
|
+
Van der Aalst et al.~\cite{vanderaalst-rpa} survey Robotic Process Automation as an enterprise paradigm for automating UI-level business processes. A systematic mapping study by Enriquez et al.~\cite{enriquez-rpa-mapping} found a ``relative lack of attention to RPA in the academic literature'' contrasting with ``early practical adoption of RPA in industry.'' Dong et al.~\cite{dong-webrobot} formalize web-based RPA in WebRobot (PLDI 2022), proposing a program synthesis algorithm for automating browser interactions evaluated on 76 benchmarks. Our work differs in using hand-written Effect TS programs rather than synthesized scripts, trading generality for the type-level guarantees needed in a production scheduling system.
|
|
129
|
+
|
|
130
|
+
\subsection{SaaS Vendor Lock-in}
|
|
131
|
+
|
|
132
|
+
Alhamazani et al.~\cite{alhamazani-lockin} and Opara-Martins et al.~\cite{opara-lockin} provide decision frameworks for assessing and mitigating SaaS vendor lock-in across API, data, and contract dimensions. Our work is a concrete case study of the scenario they describe: API access restricted by pricing tier, data accessible only through vendor UI, and no standard export mechanism for appointment history or client records.
|
|
133
|
+
|
|
134
|
+
\subsection{Containerized Browser Runtimes}
|
|
135
|
+
|
|
136
|
+
Running headless browsers in serverless environments requires careful resource management. The \texttt{@sparticuz/chromium} project~\cite{sparticuz-chromium} provides a stripped Chromium binary for AWS Lambda's 50MB deployment constraint. Microsoft's Playwright Docker images~\cite{playwright-docker} provide pre-configured containers with all browser dependencies.
|
|
137
|
+
|
|
138
|
+
The choice of container runtime significantly impacts browser automation viability (Table~\ref{tab:runtimes}). Modal's FUSE-based lazy-loading filesystem~\cite{modal-fuse} treats container images as a $\sim$5~MB index, loading file contents on demand. gVisor-based isolation~\cite{gvisor} provides a userspace kernel boundary appropriate for running untrusted browser content.
|
|
139
|
+
|
|
140
|
+
\begin{table}[htbp]
|
|
141
|
+
\caption{Serverless Platform Comparison for Browser Automation}
|
|
142
|
+
\label{tab:runtimes}
|
|
143
|
+
\centering
|
|
144
|
+
\begin{tabular}{@{}lccc@{}}
|
|
145
|
+
\toprule
|
|
146
|
+
\textbf{Characteristic} & \textbf{Modal} & \textbf{Lambda} & \textbf{Vercel} \\
|
|
147
|
+
\midrule
|
|
148
|
+
Max package size & No limit & 250 MB & 50 MB \\
|
|
149
|
+
Max memory & Config. & 10 GB & 3 GB \\
|
|
150
|
+
Max timeout & No limit & 15 min & 900s \\
|
|
151
|
+
Chromium & Native & Stripped & Impractical \\
|
|
152
|
+
\bottomrule
|
|
153
|
+
\end{tabular}
|
|
154
|
+
\end{table}
|
|
155
|
+
|
|
156
|
+
\subsection{iframe Deprecation}
|
|
157
|
+
|
|
158
|
+
The embedded iframe remains the dominant integration mechanism for SaaS scheduling widgets~\cite{acuity-embed}. However, third-party cookie deprecation in modern browsers increasingly breaks iframe-based authentication~\cite{smashing-cookies}. Approximately one-third of all breaches in 2024 were third-party related, with iframes serving as a primary attack vector~\cite{qrvey-iframe}. Migration from iframe embedding to native UI components has been documented as yielding 20--30\% higher conversion rates~\cite{cloudbeds-iframe}.
|
|
159
|
+
|
|
160
|
+
%% ============================================================
|
|
161
|
+
\section{System Design}
|
|
162
|
+
|
|
163
|
+
\subsection{Architecture Overview}
|
|
164
|
+
|
|
165
|
+
The system consists of four layers:
|
|
166
|
+
|
|
167
|
+
\textbf{Consumer Layer.} SvelteKit~\cite{sveltekit} API routes (\texttt{/api/schedule/*}) and admin pages that invoke scheduling operations through the adapter interface. Consumers are completely backend-agnostic.
|
|
168
|
+
|
|
169
|
+
\textbf{Routing Layer.} The \texttt{resolveBackend()} function selects a backend based on environment variables (\texttt{SCHEDULING\_BACKEND}) and hostname overrides (the \texttt{dev/main} branch forces Acuity for beta stability). The \texttt{getSchedulingKit()} singleton factory instantiates the selected adapter and wraps it with a \texttt{PaymentRegistry} for payment processor composition.
|
|
170
|
+
|
|
171
|
+
\textbf{Adapter Layer.} Five concrete implementations of the \texttt{SchedulingAdapter} interface: \texttt{HomegrownAdapter} (direct PostgreSQL via Drizzle ORM~\cite{drizzle}), \texttt{AcuityWizardAdapter} (Effect TS browser automation), \texttt{AcuityScraperAdapter} (read-only DOM scraping), \texttt{RemoteWizardAdapter} (HTTP proxy to Modal), and \texttt{CalComAdapter} (stub for future migration).
|
|
172
|
+
|
|
173
|
+
\textbf{Browser Middleware Layer.} Effect TS~\cite{effect-ts} programs that drive individual wizard steps (navigate, fill form, bypass payment, submit, extract confirmation), managed by a \texttt{BrowserService} layer that handles Playwright lifecycle via \texttt{acquireRelease}.
|
|
174
|
+
|
|
175
|
+
\subsection{The SchedulingAdapter Interface}
|
|
176
|
+
|
|
177
|
+
The interface defines 16 methods across five categories (Table~\ref{tab:interface}).
|
|
178
|
+
|
|
179
|
+
\begin{table}[htbp]
|
|
180
|
+
\caption{SchedulingAdapter Interface Methods}
|
|
181
|
+
\label{tab:interface}
|
|
182
|
+
\centering
|
|
183
|
+
\begin{tabular}{@{}llc@{}}
|
|
184
|
+
\toprule
|
|
185
|
+
\textbf{Category} & \textbf{Methods} & \textbf{Count} \\
|
|
186
|
+
\midrule
|
|
187
|
+
Services & \texttt{getServices}, \texttt{getService} & 2 \\
|
|
188
|
+
Providers & \texttt{getProviders}, \texttt{getProvider}, \texttt{getProvidersForService} & 3 \\
|
|
189
|
+
Availability & \texttt{getAvailableDates}, \texttt{getAvailableSlots}, \texttt{checkSlotAvailability} & 3 \\
|
|
190
|
+
Reservations & \texttt{createReservation}, \texttt{releaseReservation} & 2 \\
|
|
191
|
+
Bookings & \texttt{createBooking}, \texttt{createBookingWithPaymentRef}, \texttt{getBooking}, \texttt{cancelBooking} & 4 \\
|
|
192
|
+
Clients & \texttt{findOrCreateClient}, \texttt{getClientByEmail} & 2 \\
|
|
193
|
+
\midrule
|
|
194
|
+
\textbf{Total} & & \textbf{16} \\
|
|
195
|
+
\bottomrule
|
|
196
|
+
\end{tabular}
|
|
197
|
+
\end{table}
|
|
198
|
+
|
|
199
|
+
All methods return \texttt{SchedulingResult<T>}, defined as \texttt{TaskEither<SchedulingError, T>} from fp-ts~\cite{fp-ts}. The \texttt{SchedulingError} type is a discriminated union with seven variants (\texttt{AcuityError}, \texttt{CalComError}, \texttt{PaymentError}, \texttt{ValidationError}, \texttt{ReservationError}, \texttt{IdempotencyError}, \texttt{InfrastructureError}), each carrying a \texttt{\_tag} discriminant, a \texttt{code} string, and a human-readable \texttt{message}.
|
|
200
|
+
|
|
201
|
+
\subsection{Dual Functional Programming Architecture}
|
|
202
|
+
|
|
203
|
+
The system uses two functional programming libraries simultaneously. \textbf{Effect TS}~\cite{effect-ts} manages the browser middleware layer: generator-based programs, typed errors, dependency injection via \texttt{Context.Tag}, and resource lifecycle via \texttt{Layer.scoped} and \texttt{Effect.acquireRelease}. \textbf{fp-ts}~\cite{fp-ts} provides the adapter interface type (\texttt{TaskEither}) and composition operators (\texttt{pipe}, \texttt{chain}, \texttt{map}).
|
|
204
|
+
|
|
205
|
+
The bridge between the two is the \texttt{runEffect} function, which converts Effect programs to fp-ts \texttt{TaskEither} by running the Effect to an \texttt{Exit} value and mapping typed failures via \texttt{toSchedulingError}.
|
|
206
|
+
|
|
207
|
+
\subsection{CSS Selector Registry}
|
|
208
|
+
|
|
209
|
+
The Acuity wizard uses Emotion CSS-in-JS with hash-unstable class names. The selector registry maps 30+ logical names to arrays of CSS selector candidates tried in order. The \texttt{resolveSelector} function returns the first match within a configurable timeout, providing resilience against minor DOM restructuring.
|
|
210
|
+
|
|
211
|
+
%% ============================================================
|
|
212
|
+
\section{Implementation}
|
|
213
|
+
|
|
214
|
+
\subsection{Wizard Step Programs}
|
|
215
|
+
|
|
216
|
+
The booking creation flow is seven Effect programs:
|
|
217
|
+
|
|
218
|
+
\begin{enumerate}
|
|
219
|
+
\item \textbf{Navigate} (537 LOC). Service selection, react-calendar navigation, time slot selection.
|
|
220
|
+
\item \textbf{Fill Form} (359 LOC). Standard client fields plus React-controlled intake radio buttons (no \texttt{name}/\texttt{id} attributes---click \texttt{<label>} elements via Playwright's \texttt{locator().nth()} API).
|
|
221
|
+
\item \textbf{Bypass Payment} (226 LOC). Gift certificate coupon application to bypass Square integration.
|
|
222
|
+
\item \textbf{Submit} (168 LOC). Click confirmation with triple-detection polling (CSS selectors, URL patterns, body text).
|
|
223
|
+
\item \textbf{Extract} (174 LOC). Confirmation page scraping via regex patterns.
|
|
224
|
+
\item \textbf{Read Availability} (399 LOC). Calendar date reading with multi-month scanning.
|
|
225
|
+
\item \textbf{Read Slots} (405 LOC). Time slot extraction with AM/PM to ISO conversion.
|
|
226
|
+
\end{enumerate}
|
|
227
|
+
|
|
228
|
+
\subsection{Modal Labs Deployment}
|
|
229
|
+
|
|
230
|
+
The middleware deploys on Modal Labs~\cite{modal} using the official Playwright container image. Key configuration: 2 CPU cores, 2048~MB memory, \texttt{max\_inputs=1} per container (serialized requests prevent browser state conflicts), \texttt{min\_containers=1} for warm-pool latency. esbuild produces a single ESM bundle with all dependencies inlined except \texttt{playwright-core} (provided by the base image).
|
|
231
|
+
|
|
232
|
+
\subsection{Feature-Flag Backend Selection}
|
|
233
|
+
|
|
234
|
+
\begin{lstlisting}[language=TypeScript,caption={Backend resolution with hostname override}]
|
|
235
|
+
function resolveBackend(): 'acuity' | 'homegrown' {
|
|
236
|
+
if (env.VERCEL_GIT_COMMIT_REF === 'dev/main')
|
|
237
|
+
return 'acuity';
|
|
238
|
+
return (env.SCHEDULING_BACKEND
|
|
239
|
+
as 'acuity' | 'homegrown') ?? 'acuity';
|
|
240
|
+
}
|
|
241
|
+
\end{lstlisting}
|
|
242
|
+
|
|
243
|
+
The hostname override (\texttt{dev/main} forces Acuity) prevents accidental homegrown exposure on the beta environment while alpha runs the homegrown backend.
|
|
244
|
+
|
|
245
|
+
\subsection{The Homegrown Replacement}
|
|
246
|
+
|
|
247
|
+
The \texttt{HomegrownAdapter} implements all 16 methods via direct PostgreSQL queries through Drizzle ORM~\cite{drizzle} against Neon~\cite{neon} serverless. Design decisions include lazy database connections (factory function per-operation for Vercel cold starts), lazy schema imports (preventing ORM bundling in client code), and dual-ID resolution (UUID and legacy \texttt{acuityId} via format detection).
|
|
248
|
+
|
|
249
|
+
\subsection{Availability Engine}
|
|
250
|
+
|
|
251
|
+
A pure-function module with zero database dependency generates candidate slots at configurable intervals within business hours, filtering overlaps with occupied blocks. DST safety uses \texttt{Intl.DateTimeFormat} with named timezones. The module has 39 dedicated unit tests.
|
|
252
|
+
|
|
253
|
+
%% ============================================================
|
|
254
|
+
\section{Evaluation}
|
|
255
|
+
|
|
256
|
+
\subsection{Legacy Automation Campaign}
|
|
257
|
+
|
|
258
|
+
The browser automation layer was validated through a checkout campaign against 62 weeks of historical data (Table~\ref{tab:campaign}).
|
|
259
|
+
|
|
260
|
+
\begin{table}[htbp]
|
|
261
|
+
\caption{Checkout Automation Campaign Results}
|
|
262
|
+
\label{tab:campaign}
|
|
263
|
+
\centering
|
|
264
|
+
\begin{tabular}{@{}lr@{}}
|
|
265
|
+
\toprule
|
|
266
|
+
\textbf{Metric} & \textbf{Value} \\
|
|
267
|
+
\midrule
|
|
268
|
+
Appointments verified & 604 \\
|
|
269
|
+
Marked as paid & $\sim$245 \\
|
|
270
|
+
Discounts applied & 10 \\
|
|
271
|
+
Price corrections (bug remediation) & 2 \\
|
|
272
|
+
Circuit breaker aborts & 0 \\
|
|
273
|
+
Wrong-panel safety catches & 3 \\
|
|
274
|
+
\bottomrule
|
|
275
|
+
\end{tabular}
|
|
276
|
+
\end{table}
|
|
277
|
+
|
|
278
|
+
The ``sticky panel'' bug---where the Acuity admin panel's React state management exhibited a race condition causing wrong appointment data to persist in the detail form---was discovered during this campaign and remediated via explicit DOM removal waits, form action URL verification, and dual pre/post verification in all action functions.
|
|
279
|
+
|
|
280
|
+
\subsection{Test Coverage}
|
|
281
|
+
|
|
282
|
+
\begin{table}[htbp]
|
|
283
|
+
\caption{Test Suite Coverage}
|
|
284
|
+
\label{tab:tests}
|
|
285
|
+
\centering
|
|
286
|
+
\begin{tabular}{@{}lr@{}}
|
|
287
|
+
\toprule
|
|
288
|
+
\textbf{Component} & \textbf{Tests} \\
|
|
289
|
+
\midrule
|
|
290
|
+
Scheduling-kit (core) & 555 \\
|
|
291
|
+
Availability engine & 39 \\
|
|
292
|
+
Adapter bridge & 3 \\
|
|
293
|
+
Application (root) & 282 \\
|
|
294
|
+
\midrule
|
|
295
|
+
\textbf{Total} & \textbf{879} \\
|
|
296
|
+
\bottomrule
|
|
297
|
+
\end{tabular}
|
|
298
|
+
\end{table}
|
|
299
|
+
|
|
300
|
+
\subsection{Performance Characteristics}
|
|
301
|
+
|
|
302
|
+
\begin{table}[htbp]
|
|
303
|
+
\caption{Operation Latency Comparison}
|
|
304
|
+
\label{tab:perf}
|
|
305
|
+
\centering
|
|
306
|
+
\begin{tabular}{@{}lrrr@{}}
|
|
307
|
+
\toprule
|
|
308
|
+
\textbf{Operation} & \textbf{Browser} & \textbf{PG} & \textbf{Speedup} \\
|
|
309
|
+
\midrule
|
|
310
|
+
Get services & $\sim$8s & $<$50ms & $\sim$160$\times$ \\
|
|
311
|
+
Available dates & $\sim$18s & $<$100ms & $\sim$180$\times$ \\
|
|
312
|
+
Time slots & $\sim$18s & $<$100ms & $\sim$180$\times$ \\
|
|
313
|
+
Create booking & $\sim$45s & $<$200ms & $\sim$225$\times$ \\
|
|
314
|
+
\bottomrule
|
|
315
|
+
\end{tabular}
|
|
316
|
+
\end{table}
|
|
317
|
+
|
|
318
|
+
The two-order-of-magnitude performance gap underscores the importance of the strangler fig approach: the browser path exists to maintain service continuity during migration, not as a permanent architecture.
|
|
319
|
+
|
|
320
|
+
\subsection{Adapter Interface Coverage}
|
|
321
|
+
|
|
322
|
+
\begin{table}[htbp]
|
|
323
|
+
\caption{Adapter Method Coverage}
|
|
324
|
+
\label{tab:coverage}
|
|
325
|
+
\centering
|
|
326
|
+
\begin{tabular}{@{}lcc@{}}
|
|
327
|
+
\toprule
|
|
328
|
+
\textbf{Category} & \textbf{Wizard} & \textbf{Homegrown} \\
|
|
329
|
+
\midrule
|
|
330
|
+
Services (2) & 2/2 & 2/2 \\
|
|
331
|
+
Providers (3) & 3/3 & 3/3 \\
|
|
332
|
+
Availability (3) & 3/3 & 3/3 \\
|
|
333
|
+
Reservations (2) & 0/2 & 2/2 \\
|
|
334
|
+
Bookings (4) & 2/4 & 4/4 \\
|
|
335
|
+
Clients (2) & 2/2 & 2/2 \\
|
|
336
|
+
\midrule
|
|
337
|
+
\textbf{Total (16)} & \textbf{12/16} & \textbf{16/16} \\
|
|
338
|
+
\bottomrule
|
|
339
|
+
\end{tabular}
|
|
340
|
+
\end{table}
|
|
341
|
+
|
|
342
|
+
%% ============================================================
|
|
343
|
+
\section{Discussion}
|
|
344
|
+
|
|
345
|
+
\subsection{The Fragility Trade-off}
|
|
346
|
+
|
|
347
|
+
Browser automation against a third-party SPA is inherently fragile. Acuity can change their React component structure or Emotion CSS naming at any time. Three mitigations make this acceptable: (a) isolation via adapter interface---only the wizard adapter breaks; (b) disposability by design---the wizard adapter is a migration bridge; (c) fallback chains in the selector registry absorb minor DOM restructuring.
|
|
348
|
+
|
|
349
|
+
\subsection{Dual Effect System Complexity}
|
|
350
|
+
|
|
351
|
+
Using both Effect TS and fp-ts introduces cognitive overhead. The \texttt{runEffect} bridge is a source of subtle bugs around \texttt{FiberFailure} wrappers and \texttt{Cause} trees. A future version might unify on Effect TS throughout.
|
|
352
|
+
|
|
353
|
+
\subsection{Ethical and Legal Considerations}
|
|
354
|
+
|
|
355
|
+
Automating a vendor's UI to access data the business owns raises ethical questions. The Terms of Service may prohibit automated access, even to one's own data. The approach should be understood as a temporary bridge during migration, not a permanent circumvention of vendor access controls.
|
|
356
|
+
|
|
357
|
+
\subsection{Generalizability}
|
|
358
|
+
|
|
359
|
+
The adapter interface + browser middleware + feature-flag routing architecture generalizes to any vertical SaaS vendor providing a web UI but restricting programmatic access: CRM, billing, inventory, email marketing, and other categories where small businesses accumulate data in vendor-controlled systems.
|
|
360
|
+
|
|
361
|
+
%% ============================================================
|
|
362
|
+
\section{Conclusion}
|
|
363
|
+
|
|
364
|
+
We have presented a browser automation middleware architecture for zero-downtime migration away from a SaaS scheduling vendor that restricts API access. The architecture implements the strangler fig pattern with a 16-method adapter interface, an Effect TS browser middleware layer, and feature-flag-driven backend selection. The system has been deployed in production since March 2026, serving real appointment bookings across both backends simultaneously.
|
|
365
|
+
|
|
366
|
+
The browser middleware layer is intentionally temporary---a bridge, not a destination. The strangler fig completes not with a dramatic cutover but with the quiet deletion of the browser automation code that made the migration possible.
|
|
367
|
+
|
|
368
|
+
Future work includes completing the Acuity sunset, generalizing the middleware framework for other vertical SaaS categories, and investigating AI-assisted selector maintenance for longer-lived browser automation deployments.
|
|
369
|
+
|
|
370
|
+
%% ============================================================
|
|
371
|
+
\balance
|
|
372
|
+
\bibliographystyle{IEEEtran}
|
|
373
|
+
\bibliography{references}
|
|
374
|
+
|
|
375
|
+
\end{document}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
%%
|
|
2
|
+
%% This is file `balance.sty',
|
|
3
|
+
%% generated with the docstrip utility.
|
|
4
|
+
%%
|
|
5
|
+
%% The original source files were:
|
|
6
|
+
%%
|
|
7
|
+
%% balance.dtx (with options: `,package')
|
|
8
|
+
%%
|
|
9
|
+
%% IMPORTANT NOTICE:
|
|
10
|
+
%%
|
|
11
|
+
%% For the copyright see the source file.
|
|
12
|
+
%%
|
|
13
|
+
%% Any modified versions of this file must be renamed
|
|
14
|
+
%% with new filenames distinct from balance.sty.
|
|
15
|
+
%%
|
|
16
|
+
%% For distribution of the original source see the terms
|
|
17
|
+
%% for copying and modification in the file balance.dtx.
|
|
18
|
+
%%
|
|
19
|
+
%% This generated file may be distributed as long as the
|
|
20
|
+
%% original source files, as listed above, are part of the
|
|
21
|
+
%% same distribution. (The sources need not necessarily be
|
|
22
|
+
%% in the same archive or directory.)
|
|
23
|
+
%% Copyright 1993-1999 Patrick W Daly
|
|
24
|
+
%% Max-Planck-Institut f\"ur Aeronomie
|
|
25
|
+
%% Max-Planck-Str. 2
|
|
26
|
+
%% D-37191 Katlenburg-Lindau
|
|
27
|
+
%% Germany
|
|
28
|
+
%% E-mail: daly@linmpi.mpg.de
|
|
29
|
+
\NeedsTeXFormat{LaTeX2e}[1994/06/01]
|
|
30
|
+
\ProvidesPackage{balance}
|
|
31
|
+
[1999/02/23 4.3 (PWD)]
|
|
32
|
+
% In order to balance the columns on a page, \balance must be given
|
|
33
|
+
% somewhere within the first column. To turn off the feature, give
|
|
34
|
+
% \nobalance. One has to look at the unbalanced text first to decide
|
|
35
|
+
% where best to place \balance.
|
|
36
|
+
%-----------------------------------------------------------
|
|
37
|
+
\newcommand{\@BAlancecol}{\if@twocolumn
|
|
38
|
+
\setbox0=\vbox{\unvbox\@outputbox} \@tempdima=\ht0
|
|
39
|
+
\advance\@tempdima by \topskip \advance\@tempdima
|
|
40
|
+
by -\baselineskip \divide\@tempdima by 2
|
|
41
|
+
\splittopskip=\topskip
|
|
42
|
+
{\vbadness=\@M \loop \global\setbox3=\copy0
|
|
43
|
+
\global\setbox1=\vsplit3 to \@tempdima
|
|
44
|
+
\ifdim\ht3>\@tempdima \global\advance\@tempdima by 1pt \repeat}
|
|
45
|
+
\setbox\@leftcolumn=\vbox to \@tempdima{\unvbox1\vfil}
|
|
46
|
+
\setbox\@outputbox=\vbox to \@tempdima
|
|
47
|
+
{\dimen2=\dp3\unvbox3\kern-\dimen2
|
|
48
|
+
\vfil}
|
|
49
|
+
\fi}
|
|
50
|
+
\newif\if@BAlanceone
|
|
51
|
+
\global\@BAlanceonefalse
|
|
52
|
+
\newdimen\oldvsize
|
|
53
|
+
\newcommand{\@BAdblcol}{\if@firstcolumn
|
|
54
|
+
\unvbox\@outputbox \penalty\outputpenalty
|
|
55
|
+
\global\oldvsize=\@colht \global\multiply \@colht by 2
|
|
56
|
+
\global\@BAlanceonetrue
|
|
57
|
+
\global\@firstcolumnfalse
|
|
58
|
+
\else \global\@firstcolumntrue
|
|
59
|
+
\if@BAlanceone
|
|
60
|
+
\global\@BAlanceonefalse\@BAlancecol
|
|
61
|
+
\global\@colht=\oldvsize \else
|
|
62
|
+
\PackageWarningNoLine{balance}
|
|
63
|
+
{You have called \protect\balance\space
|
|
64
|
+
in second column\MessageBreak
|
|
65
|
+
Columns might not be balanced}\fi
|
|
66
|
+
\setbox\@outputbox\vbox to \@colht{\hbox to\textwidth
|
|
67
|
+
{\hbox to\columnwidth {\box\@leftcolumn \hss}\hfil
|
|
68
|
+
\vrule width\columnseprule\hfil \hbox to\columnwidth
|
|
69
|
+
{\box\@outputbox \hss}}\vfil}\@combinedblfloats
|
|
70
|
+
\@outputpage \begingroup \@dblfloatplacement
|
|
71
|
+
\@startdblcolumn \@whilesw\if@fcolmade \fi
|
|
72
|
+
{\@outputpage\@startdblcolumn}\endgroup
|
|
73
|
+
\fi}
|
|
74
|
+
\newcommand{\@BAcleardblpage}{\clearpage\if@twoside \ifodd\c@page\else
|
|
75
|
+
\hbox{}\newpage\fi\fi}
|
|
76
|
+
\newcommand{\@@cleardblpage}{}
|
|
77
|
+
\let\@@cleardblpage=\cleardoublepage
|
|
78
|
+
|
|
79
|
+
\newcommand{\@@utputdblcol}{}
|
|
80
|
+
\let\@@utputdblcol=\@outputdblcol
|
|
81
|
+
\newcommand{\balance}{\global\let\@outputdblcol=\@BAdblcol
|
|
82
|
+
\global\let\cleardoublepage=\@BAcleardblpage}
|
|
83
|
+
\newcommand{\nobalance}{\global\let\@outputdblcol=\@@utputdblcol
|
|
84
|
+
\global\let\cleardoublepage=\@@cleardblpage}
|
|
85
|
+
\endinput
|
|
86
|
+
%%
|
|
87
|
+
%% End of file `balance.sty'.
|