@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,231 @@
|
|
|
1
|
+
@misc{acuity-api,
|
|
2
|
+
title = {{API} Access},
|
|
3
|
+
author = {{Acuity Scheduling}},
|
|
4
|
+
year = 2026,
|
|
5
|
+
howpublished = {Acuity Scheduling Documentation},
|
|
6
|
+
note = {Requires Powerhouse plan for REST API access}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@misc{playwright,
|
|
10
|
+
title = {Playwright: Fast and Reliable End-to-End Testing for Modern Web Apps},
|
|
11
|
+
author = {{Microsoft}},
|
|
12
|
+
year = 2025,
|
|
13
|
+
howpublished = {\url{https://playwright.dev/}}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@misc{modal,
|
|
17
|
+
title = {Modal: Run Generative {AI} Models, Large-Scale Batch Jobs, Job Queues, and Much More},
|
|
18
|
+
author = {{Modal Labs}},
|
|
19
|
+
year = 2025,
|
|
20
|
+
howpublished = {\url{https://modal.com/}}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@misc{fowler-strangler,
|
|
24
|
+
title = {{StranglerFigApplication}},
|
|
25
|
+
author = {Fowler, Martin},
|
|
26
|
+
year = 2004,
|
|
27
|
+
month = jun,
|
|
28
|
+
howpublished = {\url{https://martinfowler.com/bliki/StranglerFigApplication.html}}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@techreport{sei-legacy,
|
|
32
|
+
title = {A Survey of Legacy System Modernization Approaches},
|
|
33
|
+
author = {Comella-Dorda, Santiago and Wallnau, Kurt and Seacord, Robert and Robert, John},
|
|
34
|
+
year = 2000,
|
|
35
|
+
institution = {Carnegie Mellon University, Software Engineering Institute},
|
|
36
|
+
address = {Pittsburgh, PA},
|
|
37
|
+
number = {CMU/SEI-2000-TN-003}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@book{seacord-modernizing,
|
|
41
|
+
title = {Modernizing Legacy Systems: Software Technologies, Engineering Processes and Business Practices},
|
|
42
|
+
author = {Seacord, Robert C. and Plakosh, Daniel and Lewis, Grace A.},
|
|
43
|
+
year = 2003,
|
|
44
|
+
publisher = {Addison-Wesley},
|
|
45
|
+
address = {Boston, MA}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
@inproceedings{sneed-wrapping,
|
|
49
|
+
title = {Wrapping Legacy Software for Reuse in a {SOA}},
|
|
50
|
+
author = {Sneed, Harry M.},
|
|
51
|
+
year = 2006,
|
|
52
|
+
booktitle = {Proc. Workshop on Legacy System Modernization}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@article{rahgozar-patterns,
|
|
56
|
+
title = {Design Patterns for Wrapping Similar Legacy Systems with Common Service Interfaces},
|
|
57
|
+
author = {Rahgozar, Maseud and Oroumchian, Farhad},
|
|
58
|
+
year = 2003,
|
|
59
|
+
journal = {Journal of Systems and Software}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@book{newman-microservices,
|
|
63
|
+
title = {Building Microservices},
|
|
64
|
+
author = {Newman, Sam},
|
|
65
|
+
year = 2021,
|
|
66
|
+
edition = {2nd},
|
|
67
|
+
publisher = {O'Reilly Media},
|
|
68
|
+
address = {Sebastopol, CA}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@book{evans-ddd,
|
|
72
|
+
title = {Domain-Driven Design: Tackling Complexity in the Heart of Software},
|
|
73
|
+
author = {Evans, Eric},
|
|
74
|
+
year = 2003,
|
|
75
|
+
publisher = {Addison-Wesley},
|
|
76
|
+
address = {Boston, MA}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@article{vanderaalst-rpa,
|
|
80
|
+
title = {Robotic Process Automation},
|
|
81
|
+
author = {van der Aalst, Wil M. P. and Bichler, Martin and Heinzl, Armin},
|
|
82
|
+
year = 2018,
|
|
83
|
+
journal = {Business \& Information Systems Engineering},
|
|
84
|
+
volume = 60,
|
|
85
|
+
number = 4,
|
|
86
|
+
pages = {269--272}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@inproceedings{lasso-rpa,
|
|
90
|
+
title = {Robotic Process Automation Applications Across Industries: An Exploration},
|
|
91
|
+
author = {Lasso-Rodriguez, I. and others},
|
|
92
|
+
year = 2024,
|
|
93
|
+
booktitle = {Proc. IEEE Conference}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@misc{alhamazani-lockin,
|
|
97
|
+
title = {A Holistic Decision Framework to Avoid Vendor Lock-in for Cloud {SaaS} Migration},
|
|
98
|
+
author = {Alhamazani, Omar and others},
|
|
99
|
+
year = 2017
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@misc{opara-lockin,
|
|
103
|
+
title = {A Decision Framework to Mitigate Vendor Lock-in Risks in Cloud ({SaaS} Category) Migration},
|
|
104
|
+
author = {Opara-Martins, Cyril and Sahandi, Reza and Tian, Jianguo},
|
|
105
|
+
year = 2018,
|
|
106
|
+
institution = {Bournemouth University}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@misc{sparticuz-chromium,
|
|
110
|
+
title = {{@sparticuz/chromium}},
|
|
111
|
+
author = {{Sparticuz}},
|
|
112
|
+
year = 2024,
|
|
113
|
+
howpublished = {\url{https://github.com/Sparticuz/chromium}}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@misc{playwright-docker,
|
|
117
|
+
title = {Docker | Playwright},
|
|
118
|
+
author = {{Microsoft}},
|
|
119
|
+
year = 2025,
|
|
120
|
+
howpublished = {\url{https://playwright.dev/docs/docker}}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@misc{sveltekit,
|
|
124
|
+
title = {{SvelteKit}},
|
|
125
|
+
author = {{Svelte Society}},
|
|
126
|
+
year = 2025,
|
|
127
|
+
howpublished = {\url{https://kit.svelte.dev/}}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@misc{fp-ts,
|
|
131
|
+
title = {{fp-ts}: Functional Programming in {TypeScript}},
|
|
132
|
+
author = {Scala, Giulio Canti},
|
|
133
|
+
year = 2024,
|
|
134
|
+
howpublished = {\url{https://github.com/gcanti/fp-ts}}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@misc{effect-ts,
|
|
138
|
+
title = {Effect: The Missing Standard Library for {TypeScript}},
|
|
139
|
+
author = {{Effect Contributors}},
|
|
140
|
+
year = 2025,
|
|
141
|
+
howpublished = {\url{https://effect.website/}}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@misc{drizzle,
|
|
145
|
+
title = {{Drizzle ORM}},
|
|
146
|
+
author = {{Drizzle Team}},
|
|
147
|
+
year = 2025,
|
|
148
|
+
howpublished = {\url{https://orm.drizzle.team/}}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
@misc{neon,
|
|
152
|
+
title = {Neon: Serverless {Postgres}},
|
|
153
|
+
author = {{Neon Inc.}},
|
|
154
|
+
year = 2025,
|
|
155
|
+
howpublished = {\url{https://neon.tech/}}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@book{gof-patterns,
|
|
159
|
+
title = {Design Patterns: Elements of Reusable Object-Oriented Software},
|
|
160
|
+
author = {Gamma, Erich and Helm, Richard and Johnson, Ralph and Vlissides, John},
|
|
161
|
+
year = 1994,
|
|
162
|
+
publisher = {Addison-Wesley},
|
|
163
|
+
address = {Reading, MA}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@inproceedings{khadka-legacy,
|
|
167
|
+
title = {How Do Professionals Perceive Legacy Systems and Software Modernization?},
|
|
168
|
+
author = {Khadka, Ravi and others},
|
|
169
|
+
year = 2014,
|
|
170
|
+
booktitle = {Proc. 36th Int. Conf. Software Engineering (ICSE)},
|
|
171
|
+
publisher = {ACM}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@article{enriquez-rpa-mapping,
|
|
175
|
+
title = {Robotic Process Automation: A Scientific and Industrial Systematic Mapping Study},
|
|
176
|
+
author = {Enriquez, Jos{\'e} G. and Jimenez-Ramirez, Sergio and Dominguez-Mayo, Francisco J. and Garcia-Garcia, Julian A.},
|
|
177
|
+
year = 2020,
|
|
178
|
+
journal = {IEEE Access},
|
|
179
|
+
volume = 8,
|
|
180
|
+
pages = {39113--39129}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
@inproceedings{dong-webrobot,
|
|
184
|
+
title = {{WebRobot}: Web Robotic Process Automation using Interactive Programming-by-Demonstration},
|
|
185
|
+
author = {Dong, Xiang and Chen, Zhenhua and Wen, Yifan and Cheung, Alvin},
|
|
186
|
+
year = 2022,
|
|
187
|
+
booktitle = {Proc. ACM SIGPLAN Conf. Programming Language Design and Implementation (PLDI)}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@misc{modal-fuse,
|
|
191
|
+
title = {Fast, Lazy Container Loading},
|
|
192
|
+
author = {{Modal Labs}},
|
|
193
|
+
year = 2024,
|
|
194
|
+
howpublished = {\url{https://modal.com/blog/jono-containers-talk}}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@misc{gvisor,
|
|
198
|
+
title = {{gVisor}: Container Runtime Sandbox},
|
|
199
|
+
author = {{Google}},
|
|
200
|
+
year = 2024,
|
|
201
|
+
howpublished = {\url{https://gvisor.dev/}}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@misc{acuity-embed,
|
|
205
|
+
title = {Embeds and Dynamic Links},
|
|
206
|
+
author = {{Acuity Scheduling}},
|
|
207
|
+
year = 2026,
|
|
208
|
+
howpublished = {\url{https://developers.acuityscheduling.com/docs/embedding}}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@misc{smashing-cookies,
|
|
212
|
+
title = {Reliably Detecting Third-Party Cookie Blocking In 2025},
|
|
213
|
+
author = {{Smashing Magazine}},
|
|
214
|
+
year = 2025,
|
|
215
|
+
month = may,
|
|
216
|
+
howpublished = {\url{https://www.smashingmagazine.com/2025/05/reliably-detecting-third-party-cookie-blocking-2025/}}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@misc{qrvey-iframe,
|
|
220
|
+
title = {2026 Iframe Security Risks and 10 Ways to Secure Them},
|
|
221
|
+
author = {{Qrvey}},
|
|
222
|
+
year = 2026,
|
|
223
|
+
howpublished = {\url{https://qrvey.com/blog/iframe-security/}}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
@misc{cloudbeds-iframe,
|
|
227
|
+
title = {iFrame Deprecation: Upgrade to Cloudbeds Booking Engine Plus},
|
|
228
|
+
author = {{Cloudbeds}},
|
|
229
|
+
year = 2025,
|
|
230
|
+
howpublished = {\url{https://myfrontdesk.cloudbeds.com/hc/en-us/articles/42963882806299}}
|
|
231
|
+
}
|
package/docs/paper.md
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
# Browser Automation Middleware for Zero-Downtime SaaS Migration: A Scheduling System Case Study
|
|
2
|
+
|
|
3
|
+
**Jess Sullivan**
|
|
4
|
+
Tinyland Inc.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Abstract
|
|
9
|
+
|
|
10
|
+
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.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## I. Introduction
|
|
15
|
+
|
|
16
|
+
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.
|
|
17
|
+
|
|
18
|
+
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 [1]. The business's own appointment history, client records, and scheduling configuration are accessible only through the web UI.
|
|
19
|
+
|
|
20
|
+
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 [2], translating standardized adapter method calls into sequences of DOM interactions. The middleware is deployed as a containerized service on Modal Labs [3], providing serverless scaling with warm container pools for acceptable latency.
|
|
21
|
+
|
|
22
|
+
The key insight is that browser automation middleware is not the destination -- it is the bridge. The architecture implements the strangler fig pattern [4] 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.
|
|
23
|
+
|
|
24
|
+
### Contributions
|
|
25
|
+
|
|
26
|
+
This paper makes four contributions:
|
|
27
|
+
|
|
28
|
+
1. A formal 16-method `SchedulingAdapter` interface that abstracts scheduling operations (services, availability, reservations, bookings, clients) across heterogeneous backends, with all methods returning monadic `TaskEither<SchedulingError, T>` for composable error handling.
|
|
29
|
+
|
|
30
|
+
2. 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 `acquireRelease`, and a CSS selector registry with fallback chains for resilience against DOM instability.
|
|
31
|
+
|
|
32
|
+
3. A feature-flag-driven backend selection mechanism with hostname override, enabling both backends to serve production traffic simultaneously with instant rollback capability.
|
|
33
|
+
|
|
34
|
+
4. Production deployment evidence from a real massage therapy practice, including reliability data from 604 automated legacy appointment operations and concurrent dual-backend operation.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## II. Related Work
|
|
39
|
+
|
|
40
|
+
### A. Legacy System Modernization
|
|
41
|
+
|
|
42
|
+
The Carnegie Mellon Software Engineering Institute taxonomy [5] 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. [6] 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.
|
|
43
|
+
|
|
44
|
+
### B. Wrapper-Based Evolution
|
|
45
|
+
|
|
46
|
+
Sneed [7] 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 [8] identify three design patterns for wrapper interfaces: Lowest Common Denominator, Most Popular, and Negotiated. Our `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.
|
|
47
|
+
|
|
48
|
+
Prior wrapper work focused on mainframe terminal interfaces (3270 screen scraping) and COBOL API wrapping [5]. 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.
|
|
49
|
+
|
|
50
|
+
### C. The Strangler Fig Pattern
|
|
51
|
+
|
|
52
|
+
Fowler [4] 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 [9] applies the pattern to microservice migration. Our `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).
|
|
53
|
+
|
|
54
|
+
### D. Anti-Corruption Layer
|
|
55
|
+
|
|
56
|
+
Evans [10] defines the Anti-Corruption Layer as a pattern for isolating a bounded context from the domain model of an external system. The `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 `acuityId` -- is an explicit corruption-containment mechanism.
|
|
57
|
+
|
|
58
|
+
### E. Robotic Process Automation
|
|
59
|
+
|
|
60
|
+
Van der Aalst et al. [11] survey Robotic Process Automation as an enterprise paradigm for automating UI-level business processes. The 2024 IEEE survey by Lasso-Rodriguez et al. [12] extends this analysis across industry verticals. A systematic mapping study by Enriquez et al. [24] found a "relative lack of attention to RPA in the academic literature" contrasting with "early practical adoption of RPA in industry." Our work shares the automation-of-UI-interaction philosophy but differs in three ways: (a) typed interfaces with monadic error handling replace low-code bot builders, (b) the automation target is a third-party SaaS UI rather than an internal enterprise application, and (c) the automation is explicitly designed to be temporary -- a bridge to a replacement backend, not a permanent integration layer.
|
|
61
|
+
|
|
62
|
+
Dong et al. [25] formalize web-based RPA in WebRobot (PLDI 2022), proposing a program synthesis algorithm for automating browser interactions. Evaluated on 76 benchmarks, WebRobot demonstrates the feasibility of treating browser automation as a programming language construct. 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.
|
|
63
|
+
|
|
64
|
+
### F. SaaS Vendor Lock-in
|
|
65
|
+
|
|
66
|
+
Alhamazani et al. [13] and Opara-Martins et al. [14] 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.
|
|
67
|
+
|
|
68
|
+
### G. Containerized Browser Runtimes
|
|
69
|
+
|
|
70
|
+
Running headless browsers in serverless environments requires careful resource management. The `@sparticuz/chromium` project [15] provides a stripped Chromium binary for AWS Lambda's 50MB deployment constraint. Microsoft's Playwright Docker images [16] provide pre-configured containers with all browser dependencies.
|
|
71
|
+
|
|
72
|
+
The choice of container runtime significantly impacts browser automation viability:
|
|
73
|
+
|
|
74
|
+
| Platform | Max Package Size | Max Memory | Max Timeout | Chromium Support |
|
|
75
|
+
|----------|-----------------|------------|-------------|-----------------|
|
|
76
|
+
| Modal Labs | No practical limit (FUSE lazy-load) | Configurable | No hard limit | Native via `playwright install` |
|
|
77
|
+
| AWS Lambda | 50 MB zipped / 250 MB uncompressed | 10 GB | 15 min | Requires @sparticuz/chromium |
|
|
78
|
+
| Vercel Functions | 50 MB | 1-3 GB | 10-900s | Impractical at size limits |
|
|
79
|
+
|
|
80
|
+
Modal's architecture uses a FUSE-based lazy-loading filesystem [26] that treats container images as an index (~5 MB of metadata), loading file contents on demand. This eliminates size constraints that make Lambda and Vercel problematic for browser workloads. Modal's gVisor-based isolation [27] provides a userspace kernel boundary appropriate for running untrusted browser content, and memory snapshots enable sub-3-second cold starts even with heavy dependencies.
|
|
81
|
+
|
|
82
|
+
### H. iframe Deprecation
|
|
83
|
+
|
|
84
|
+
The embedded iframe remains the dominant integration mechanism for SaaS scheduling widgets [28]. However, third-party cookie deprecation in modern browsers increasingly breaks iframe-based authentication, resulting in blank screens or non-functional login prompts [29]. Approximately one-third of all breaches in 2024 were third-party related, with iframes serving as a primary attack vector [30]. The migration from iframe embedding to native UI components -- documented by Cloudbeds as yielding 20-30% higher conversion rates [31] -- represents the broader industry context motivating this work.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## III. System Design
|
|
89
|
+
|
|
90
|
+
### A. Architecture Overview
|
|
91
|
+
|
|
92
|
+
The system consists of four layers:
|
|
93
|
+
|
|
94
|
+
**Consumer Layer.** SvelteKit [17] API routes (`/api/schedule/*`) and admin pages that invoke scheduling operations through the adapter interface. Consumers are completely backend-agnostic.
|
|
95
|
+
|
|
96
|
+
**Routing Layer.** The `resolveBackend()` function selects a backend based on environment variables (`SCHEDULING_BACKEND`) and hostname overrides (the `dev/main` branch forces Acuity for beta stability). The `getSchedulingKit()` singleton factory instantiates the selected adapter and wraps it with a `PaymentRegistry` for payment processor composition.
|
|
97
|
+
|
|
98
|
+
**Adapter Layer.** Five concrete implementations of the `SchedulingAdapter` interface:
|
|
99
|
+
- `HomegrownAdapter`: Direct PostgreSQL queries via Drizzle ORM against Neon serverless
|
|
100
|
+
- `AcuityWizardAdapter`: Effect TS browser automation via Playwright
|
|
101
|
+
- `AcuityScraperAdapter`: Read-only DOM scraping for service/availability data
|
|
102
|
+
- `RemoteWizardAdapter`: HTTP proxy to a remote middleware server
|
|
103
|
+
- `CalComAdapter`: Stub for potential future migration (all methods return NOT_IMPLEMENTED)
|
|
104
|
+
|
|
105
|
+
**Browser Middleware Layer.** Effect TS programs that drive individual wizard steps (navigate, fill form, bypass payment, submit, extract confirmation), managed by a `BrowserService` layer that handles Playwright lifecycle via `acquireRelease`.
|
|
106
|
+
|
|
107
|
+
### B. The SchedulingAdapter Interface
|
|
108
|
+
|
|
109
|
+
The interface defines 16 methods across five categories:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
interface SchedulingAdapter {
|
|
113
|
+
// Services (2)
|
|
114
|
+
getServices(): SchedulingResult<Service[]>;
|
|
115
|
+
getService(id: string): SchedulingResult<Service>;
|
|
116
|
+
// Providers (3)
|
|
117
|
+
getProviders(): SchedulingResult<Provider[]>;
|
|
118
|
+
getProvider(id: string): SchedulingResult<Provider>;
|
|
119
|
+
getProvidersForService(id: string): SchedulingResult<Provider[]>;
|
|
120
|
+
// Availability (3)
|
|
121
|
+
getAvailableDates(p: DateParams): SchedulingResult<AvailableDate[]>;
|
|
122
|
+
getAvailableSlots(p: SlotParams): SchedulingResult<TimeSlot[]>;
|
|
123
|
+
checkSlotAvailability(p: CheckParams): SchedulingResult<boolean>;
|
|
124
|
+
// Reservations (2)
|
|
125
|
+
createReservation(p: ReserveParams): SchedulingResult<SlotReservation>;
|
|
126
|
+
releaseReservation(id: string): SchedulingResult<void>;
|
|
127
|
+
// Bookings (4)
|
|
128
|
+
createBooking(req: BookingRequest): SchedulingResult<Booking>;
|
|
129
|
+
createBookingWithPaymentRef(req, ref, proc): SchedulingResult<Booking>;
|
|
130
|
+
getBooking(id: string): SchedulingResult<Booking>;
|
|
131
|
+
cancelBooking(id: string, reason?: string): SchedulingResult<void>;
|
|
132
|
+
// Clients (2)
|
|
133
|
+
findOrCreateClient(c: ClientInfo): SchedulingResult<ClientInfo>;
|
|
134
|
+
getClientByEmail(email: string): SchedulingResult<ClientInfo | null>;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
All methods return `SchedulingResult<T>`, defined as `TaskEither<SchedulingError, T>` from fp-ts [18]. The `SchedulingError` type is a discriminated union with seven variants (`AcuityError`, `CalComError`, `PaymentError`, `ValidationError`, `ReservationError`, `IdempotencyError`, `InfrastructureError`), each carrying a `_tag` discriminant, a `code` string, and a human-readable `message`.
|
|
139
|
+
|
|
140
|
+
This design forces all error handling to be explicit at the type level. Consumers cannot accidentally ignore errors -- they must fold over the `Either` to access the success value. The `TaskEither` wrapper (a thunk returning `Promise<Either<E, A>>`) enables lazy evaluation and composition via `pipe`, `chain`, and `map`.
|
|
141
|
+
|
|
142
|
+
### C. Dual Functional Programming Architecture
|
|
143
|
+
|
|
144
|
+
The system uses two functional programming libraries simultaneously:
|
|
145
|
+
|
|
146
|
+
**Effect TS** [19] manages the browser middleware layer. Effect programs are generator-based (`Effect.gen(function* () { ... })`), with typed errors, dependency injection via `Context.Tag`, and resource lifecycle management via `Layer.scoped` and `Effect.acquireRelease`. The browser and page are acquired resources that are guaranteed to close on scope exit, even if the wizard step programs fail.
|
|
147
|
+
|
|
148
|
+
**fp-ts** [18] provides the adapter interface type (`TaskEither`) and the composition operators (`pipe`, `chain`, `map`, `tap`) used by consumers and the booking pipeline. The adapter interface is defined in fp-ts terms because it predates the middleware layer and because fp-ts `TaskEither` is simpler to consume than Effect for callers who do not need resource management.
|
|
149
|
+
|
|
150
|
+
The bridge between the two is the `runEffect` function in the wizard adapter, which converts an Effect program to a `TaskEither` by running the Effect to an `Exit` value and mapping `Success` to `Right` and typed `Failure` to `Left` via the `toSchedulingError` bridge function.
|
|
151
|
+
|
|
152
|
+
### D. CSS Selector Registry
|
|
153
|
+
|
|
154
|
+
The Acuity booking wizard is a React SPA using Emotion CSS-in-JS, which generates hash-based class names (e.g., `css-1a2b3c4`) that are unstable across deployments. The selector registry (`Selectors`) maps 30+ logical names to arrays of CSS selector candidates, tried in order:
|
|
155
|
+
|
|
156
|
+
```typescript
|
|
157
|
+
const Selectors = {
|
|
158
|
+
serviceList: [
|
|
159
|
+
'.select-container .select-item',
|
|
160
|
+
'[class*="service-list"] [class*="service-item"]',
|
|
161
|
+
'.appointment-type-list .appointment-type',
|
|
162
|
+
],
|
|
163
|
+
submitButton: [
|
|
164
|
+
'button[type="submit"]:not([disabled])',
|
|
165
|
+
'button.btn-primary:not([disabled])',
|
|
166
|
+
'[data-testid="submit-booking"]',
|
|
167
|
+
// ... 5 more fallbacks
|
|
168
|
+
],
|
|
169
|
+
// ... 28 more keys
|
|
170
|
+
};
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The `resolveSelector` function tries each candidate with a configurable timeout, returning the first match. The `probeSelector` variant returns `null` instead of failing, used for optional UI elements. This fallback-chain pattern provides resilience against minor DOM restructuring while maintaining the expectation that major UI redesigns will require registry updates.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## IV. Implementation
|
|
178
|
+
|
|
179
|
+
### A. Wizard Step Programs
|
|
180
|
+
|
|
181
|
+
The booking creation flow is implemented as seven Effect programs, each targeting a distinct phase of the Acuity wizard:
|
|
182
|
+
|
|
183
|
+
1. **Navigate** (537 LOC). Loads the service selection page, matches the target service by name, clicks "Book," navigates the react-calendar to the target month (up to 12 iterations), clicks the target day tile, selects the time slot, and clicks "Select and continue." The calendar month detection parses label text with the regex `([A-Za-z]+)\s*(\d{4})` to handle whitespace variation. Day tiles are filtered by checking for the `neighboringMonth` CSS class and the `disabled` property.
|
|
184
|
+
|
|
185
|
+
2. **Fill Form** (359 LOC). Fills standard client fields (`firstName`, `lastName`, `email`, `phone`) using `input[name="client.X"]` selectors with smart-fill (checks current value, skips if correct). Handles React-controlled intake radio buttons that lack `name` or `id` attributes by clicking `<label>` elements via Playwright's `locator().nth()` API, dispatching OS-level mouse events that React's event delegation processes. Fills medication textarea and terms checkbox.
|
|
186
|
+
|
|
187
|
+
3. **Bypass Payment** (226 LOC). Clicks the "Package, gift, or coupon code" toggle, enters a 100% gift certificate code from the `ACUITY_BYPASS_COUPON` environment variable, clicks "Apply," and verifies the discount was applied by checking for "Gift certificate" text and a "-$" indicator. This decouples scheduling from Acuity's Square payment integration -- actual payment is handled externally via Stripe, Venmo, or cash.
|
|
188
|
+
|
|
189
|
+
4. **Submit** (168 LOC). Clicks "PAY & CONFIRM" (8 fallback selectors) and polls for confirmation using three detection methods: CSS selectors (`.confirmation`, `.booking-confirmed`, etc.), URL pattern matching (`/(confirmation|confirmed|thank-you|complete)/i`), and body text searching ("booking confirmed", "appointment confirmed"). Polls every second for up to 60 seconds. Retries once with 2-second backoff on transient navigation failures.
|
|
190
|
+
|
|
191
|
+
5. **Extract** (174 LOC). Scrapes confirmation data (confirmation code, service name, datetime) from the confirmation page using regex patterns, maps to the `Booking` domain type.
|
|
192
|
+
|
|
193
|
+
6. **Read Availability** (399 LOC). Navigates to a service's calendar and reads enabled tiles to determine available dates. Multi-month scanning supported via prev/next navigation.
|
|
194
|
+
|
|
195
|
+
7. **Read Slots** (405 LOC). Clicks a specific date tile and reads `button.time-selection` elements. Slot text concatenates time and availability ("10:00 AM1 spot left") -- the regex `^(\d{1,2}:\d{2}\s*[AP]M)` extracts the time prefix.
|
|
196
|
+
|
|
197
|
+
### B. BrowserService Layer
|
|
198
|
+
|
|
199
|
+
The `BrowserService` is an Effect `Context.Tag` providing managed Playwright resources:
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
class BrowserService extends Context.Tag('BrowserService')<
|
|
203
|
+
BrowserService, BrowserServiceShape
|
|
204
|
+
>() {}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The live implementation uses two nested `acquireRelease` pairs: the outer pair manages the browser process (launch on acquire, close on release), the inner pair manages the page instance. Both are scoped to the `Layer` lifetime. When any wizard step program fails -- selector timeout, navigation error, coupon rejection -- the scope unwinds and both resources are released in reverse order.
|
|
208
|
+
|
|
209
|
+
### C. Modal Labs Deployment
|
|
210
|
+
|
|
211
|
+
The middleware is deployed on Modal Labs [3] using the official Playwright Docker image as a base. The deployment configuration:
|
|
212
|
+
|
|
213
|
+
- **Image**: `mcr.microsoft.com/playwright:v1.58.2-noble` with Node.js 22 LTS
|
|
214
|
+
- **Resources**: 2 CPU cores, 2048 MB memory, no GPU
|
|
215
|
+
- **Concurrency**: `max_inputs=1` per container (serialized requests prevent browser state conflicts)
|
|
216
|
+
- **Scaling**: `min_containers=1` for warm-pool latency reduction
|
|
217
|
+
- **Timeout**: 300 seconds (wizard flows take 15-60 seconds depending on operation)
|
|
218
|
+
- **Bundling**: esbuild produces a single `server.mjs` with all dependencies inlined except `playwright-core` (provided by base image), eliminating `node_modules` for faster cold starts
|
|
219
|
+
|
|
220
|
+
The `max_inputs=1` serialization is a key architectural decision. Each browser session maintains page-level state (navigation history, cookies, form data). Concurrent requests on the same page instance would cause race conditions. Modal horizontally scales by spawning additional containers, each with its own isolated browser instance.
|
|
221
|
+
|
|
222
|
+
### D. Feature-Flag Backend Selection
|
|
223
|
+
|
|
224
|
+
The `resolveBackend()` function implements a priority chain:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
function resolveBackend(): 'acuity' | 'homegrown' {
|
|
228
|
+
if (env.VERCEL_GIT_COMMIT_REF === 'dev/main') return 'acuity';
|
|
229
|
+
return (env.SCHEDULING_BACKEND as 'acuity' | 'homegrown') ?? 'acuity';
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The hostname override (`dev/main` forces Acuity) prevents accidental homegrown exposure on the beta environment while the alpha environment (`dev/*` branches) runs the homegrown backend. This implements a two-phase migration: shadow reads on alpha, then full cutover when the homegrown backend reaches feature parity.
|
|
234
|
+
|
|
235
|
+
### E. The Homegrown Replacement
|
|
236
|
+
|
|
237
|
+
The `HomegrownAdapter` implements all 16 `SchedulingAdapter` methods via direct PostgreSQL queries through Drizzle ORM [20] against Neon serverless [21]. Key design decisions:
|
|
238
|
+
|
|
239
|
+
- **Lazy database connection**: The adapter receives a `getDb` factory function, called per-operation. This avoids import-time connections critical for Vercel cold starts.
|
|
240
|
+
- **Lazy schema imports**: Drizzle schema tables are dynamically imported inside each method, preventing the ORM runtime from being bundled into client-side code.
|
|
241
|
+
- **Dual-ID resolution**: `resolveService` accepts both UUID (homegrown) and integer `acuityId` (legacy) via UUID format detection, enabling backward compatibility during migration.
|
|
242
|
+
- **Real slot reservations**: PG-persisted with `expires_at`, unlike the wizard adapter which cannot support reservations through the public UI.
|
|
243
|
+
|
|
244
|
+
### F. Availability Engine
|
|
245
|
+
|
|
246
|
+
The availability engine is a pure-function module with zero database dependency. All data is passed as arguments. The core algorithm generates candidate slots at configurable intervals within business hours, filters out those overlapping with occupied blocks (bookings + time blocks + active reservations), applies buffer time and minimum advance notice constraints. DST safety is achieved via `Intl.DateTimeFormat` with named timezones -- no manual offset tables. The module has 39 dedicated unit tests.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## V. Evaluation
|
|
251
|
+
|
|
252
|
+
### A. Legacy Automation Campaign
|
|
253
|
+
|
|
254
|
+
The browser automation layer was validated through a checkout automation campaign against 62 weeks of historical calendar data:
|
|
255
|
+
|
|
256
|
+
| Metric | Value |
|
|
257
|
+
|--------|-------|
|
|
258
|
+
| Appointments verified | 604 |
|
|
259
|
+
| Marked as paid | ~245 |
|
|
260
|
+
| Discounts applied | 10 (Liz Hartman, $100 to $70) |
|
|
261
|
+
| Price corrections | 2 (sticky panel bug remediation) |
|
|
262
|
+
| Circuit breaker aborts | 0 (after bug fixes) |
|
|
263
|
+
| Wrong-panel safety catches | 3 (all caught, corrected) |
|
|
264
|
+
|
|
265
|
+
The "sticky panel" bug -- where the Acuity admin panel's React state management exhibited a race condition causing the wrong appointment's data to persist in the detail form -- was discovered during this campaign. The remediation required three changes: explicit DOM removal waits on panel close, form action URL verification on panel open, and dual pre/post verification in all action functions.
|
|
266
|
+
|
|
267
|
+
### B. Test Coverage
|
|
268
|
+
|
|
269
|
+
| Component | Tests | Coverage Focus |
|
|
270
|
+
|-----------|-------|---------------|
|
|
271
|
+
| Scheduling-kit (core) | 555 | Adapter interface, pipeline composition, payment integration |
|
|
272
|
+
| Availability engine | 39 | Slot generation, DST, overlap detection, buffer time, edge cases |
|
|
273
|
+
| Adapter bridge | 3 | Backend selection, ID resolution |
|
|
274
|
+
| Application (root) | 282 | Route handlers, form validation, component rendering |
|
|
275
|
+
| **Total** | **879** | |
|
|
276
|
+
|
|
277
|
+
### C. Performance Characteristics
|
|
278
|
+
|
|
279
|
+
| Operation | Browser Middleware | Homegrown (PG) | Speedup |
|
|
280
|
+
|-----------|-------------------|----------------|---------|
|
|
281
|
+
| Get services | ~8s (DOM scrape) | <50ms | ~160x |
|
|
282
|
+
| Get available dates | ~15-20s (wizard nav) | <100ms | ~150-200x |
|
|
283
|
+
| Get time slots | ~15-20s (wizard nav) | <100ms | ~150-200x |
|
|
284
|
+
| Create booking | ~30-60s (full wizard) | <200ms | ~150-300x |
|
|
285
|
+
|
|
286
|
+
The browser middleware latency is acceptable only as a migration bridge. 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.
|
|
287
|
+
|
|
288
|
+
### D. Adapter Interface Coverage
|
|
289
|
+
|
|
290
|
+
| Category | Methods | Wizard Adapter | Homegrown Adapter |
|
|
291
|
+
|----------|---------|----------------|-------------------|
|
|
292
|
+
| Services | 2 | 2/2 | 2/2 |
|
|
293
|
+
| Providers | 3 | 3/3 (hardcoded) | 3/3 |
|
|
294
|
+
| Availability | 3 | 3/3 | 3/3 |
|
|
295
|
+
| Reservations | 2 | 0/2 (graceful fail) | 2/2 |
|
|
296
|
+
| Bookings | 4 | 2/4 | 4/4 |
|
|
297
|
+
| Clients | 2 | 2/2 (pass-through) | 2/2 |
|
|
298
|
+
| **Total** | **16** | **12/16** | **16/16** |
|
|
299
|
+
|
|
300
|
+
The wizard adapter's incomplete coverage (no `getBooking`, `cancelBooking`, slot reservations) reflects the limitations of the public booking UI -- these operations require admin panel access. The homegrown adapter's full coverage demonstrates feature parity.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## VI. Discussion
|
|
305
|
+
|
|
306
|
+
### A. The Fragility Trade-off
|
|
307
|
+
|
|
308
|
+
Browser automation against a third-party SPA is inherently fragile. Acuity can change their React component structure, Emotion CSS class naming, or wizard flow at any time, breaking the selector registry. Three mitigations make this acceptable:
|
|
309
|
+
|
|
310
|
+
1. **Isolation via adapter interface.** Only the wizard adapter breaks -- all consumers continue to function via the homegrown backend.
|
|
311
|
+
2. **Disposability by design.** The wizard adapter is a migration bridge. Once the homegrown backend reaches full feature parity, the browser middleware layer is deleted entirely.
|
|
312
|
+
3. **Fallback chains.** The selector registry tries multiple candidates per logical element, absorbing minor DOM restructuring without code changes.
|
|
313
|
+
|
|
314
|
+
### B. Dual Effect System Complexity
|
|
315
|
+
|
|
316
|
+
Using both Effect TS and fp-ts introduces cognitive overhead. The `runEffect` bridge function is a source of subtle bugs -- incorrectly handling `FiberFailure` wrappers or `Cause` trees can swallow typed errors. In hindsight, a single effect system would be preferable. The dual approach emerged from the adapter interface (defined in fp-ts terms before the middleware layer was built) and the middleware layer (requiring Effect's resource management capabilities). A future version might unify on Effect TS throughout.
|
|
317
|
+
|
|
318
|
+
### C. Ethical and Legal Considerations
|
|
319
|
+
|
|
320
|
+
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. We note that the data being accessed -- appointment schedules, client contact information, business hours -- belongs to the business, not the vendor. The automation accesses only the public booking wizard and the authenticated admin panel, never bypassing access controls. The coupon-based payment bypass is a legitimate use of Acuity's own gift certificate feature. Nevertheless, the approach should be understood as a temporary bridge during migration, not a permanent circumvention of vendor access controls.
|
|
321
|
+
|
|
322
|
+
### D. Generalizability
|
|
323
|
+
|
|
324
|
+
The pattern applies to any vertical SaaS vendor that provides a web UI but restricts programmatic access. The adapter interface + browser middleware + feature-flag routing architecture generalizes across domains: CRM, billing, inventory, email marketing, and other categories where small businesses accumulate data in vendor-controlled systems. The key prerequisite is that the vendor's web UI must be automatable -- single-page applications with predictable DOM structure are more tractable than server-rendered pages with CSRF tokens and captchas.
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## VII. Conclusion
|
|
329
|
+
|
|
330
|
+
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 the legacy vendor (via browser automation) and a homegrown PostgreSQL backend (via direct queries) simultaneously.
|
|
331
|
+
|
|
332
|
+
The browser middleware layer is intentionally temporary -- a bridge, not a destination. As the homegrown backend reaches full feature parity (currently 16/16 adapter methods), the middleware becomes disposable. The adapter interface ensures consumers are completely isolated from this transition. The strangler fig completes not with a dramatic cutover but with the quiet deletion of the browser automation code that made the migration possible.
|
|
333
|
+
|
|
334
|
+
Future work includes completing the Acuity sunset (removing the wizard adapter once dual-write reconciliation confirms data consistency), generalizing the middleware framework for other vertical SaaS categories, and investigating AI-assisted selector maintenance for longer-lived browser automation deployments.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## References
|
|
339
|
+
|
|
340
|
+
[1] Acuity Scheduling, "API Access," *Acuity Scheduling Documentation*, 2026. [Requires Powerhouse plan for REST API access.]
|
|
341
|
+
|
|
342
|
+
[2] Microsoft, "Playwright: Fast and reliable end-to-end testing for modern web apps," 2025. [Online]. Available: https://playwright.dev/
|
|
343
|
+
|
|
344
|
+
[3] Modal Labs, "Modal: Run generative AI models, large-scale batch jobs, job queues, and much more," 2025. [Online]. Available: https://modal.com/
|
|
345
|
+
|
|
346
|
+
[4] M. Fowler, "StranglerFigApplication," *martinfowler.com*, Jun. 2004. [Online]. Available: https://martinfowler.com/bliki/StranglerFigApplication.html
|
|
347
|
+
|
|
348
|
+
[5] S. Comella-Dorda, K. Wallnau, R. Seacord, and J. Robert, "A Survey of Legacy System Modernization Approaches," Carnegie Mellon Univ., Software Eng. Inst., Pittsburgh, PA, Tech. Note CMU/SEI-2000-TN-003, 2000.
|
|
349
|
+
|
|
350
|
+
[6] R. C. Seacord, D. Plakosh, and G. A. Lewis, *Modernizing Legacy Systems: Software Technologies, Engineering Processes and Business Practices*. Boston, MA: Addison-Wesley, 2003.
|
|
351
|
+
|
|
352
|
+
[7] H. M. Sneed, "Wrapping legacy software for reuse in a SOA," in *Proc. Workshop on Legacy System Modernization*, 2006.
|
|
353
|
+
|
|
354
|
+
[8] M. Rahgozar and A. Oroumchian, "Design Patterns for Wrapping Similar Legacy Systems with Common Service Interfaces," *J. Syst. Softw.*, 2003.
|
|
355
|
+
|
|
356
|
+
[9] S. Newman, *Building Microservices*, 2nd ed. Sebastopol, CA: O'Reilly Media, 2021.
|
|
357
|
+
|
|
358
|
+
[10] E. Evans, *Domain-Driven Design: Tackling Complexity in the Heart of Software*. Boston, MA: Addison-Wesley, 2003.
|
|
359
|
+
|
|
360
|
+
[11] W. M. P. van der Aalst, M. Bichler, and A. Heinzl, "Robotic Process Automation," *Bus. Inf. Syst. Eng.*, vol. 60, no. 4, pp. 269-272, 2018.
|
|
361
|
+
|
|
362
|
+
[12] I. Lasso-Rodriguez et al., "Robotic Process Automation Applications Across Industries: An Exploration," in *Proc. IEEE Conf.*, 2024.
|
|
363
|
+
|
|
364
|
+
[13] O. Alhamazani et al., "A Holistic Decision Framework to Avoid Vendor Lock-in for Cloud SaaS Migration," 2017.
|
|
365
|
+
|
|
366
|
+
[14] C. Opara-Martins, R. Sahandi, and J. Tian, "A decision framework to mitigate vendor lock-in risks in cloud (SaaS category) migration," Bournemouth Univ., 2018.
|
|
367
|
+
|
|
368
|
+
[15] Sparticuz, "@sparticuz/chromium," GitHub, 2024. [Online]. Available: https://github.com/Sparticuz/chromium
|
|
369
|
+
|
|
370
|
+
[16] Microsoft, "Docker | Playwright," *playwright.dev*, 2025. [Online]. Available: https://playwright.dev/docs/docker
|
|
371
|
+
|
|
372
|
+
[17] Svelte Society, "SvelteKit," 2025. [Online]. Available: https://kit.svelte.dev/
|
|
373
|
+
|
|
374
|
+
[18] G. C. Scala, "fp-ts: Functional programming in TypeScript," GitHub, 2024. [Online]. Available: https://github.com/gcanti/fp-ts
|
|
375
|
+
|
|
376
|
+
[19] Effect Contributors, "Effect: The missing standard library for TypeScript," 2025. [Online]. Available: https://effect.website/
|
|
377
|
+
|
|
378
|
+
[20] Drizzle Team, "Drizzle ORM," 2025. [Online]. Available: https://orm.drizzle.team/
|
|
379
|
+
|
|
380
|
+
[21] Neon Inc., "Neon: Serverless Postgres," 2025. [Online]. Available: https://neon.tech/
|
|
381
|
+
|
|
382
|
+
[22] E. Gamma, R. Helm, R. Johnson, and J. Vlissides, *Design Patterns: Elements of Reusable Object-Oriented Software*. Reading, MA: Addison-Wesley, 1994.
|
|
383
|
+
|
|
384
|
+
[23] R. Khadka et al., "How do professionals perceive legacy systems and software modernization?," in *Proc. 36th Int. Conf. Softw. Eng. (ICSE)*, ACM, 2014.
|
|
385
|
+
|
|
386
|
+
[24] J. G. Enriquez, S. Jimenez-Ramirez, F. J. Dominguez-Mayo, and J. A. Garcia-Garcia, "Robotic Process Automation: A Scientific and Industrial Systematic Mapping Study," *IEEE Access*, vol. 8, pp. 39113-39129, 2020.
|
|
387
|
+
|
|
388
|
+
[25] X. Dong, Z. Chen, Y. Wen, and A. Cheung, "WebRobot: Web Robotic Process Automation using Interactive Programming-by-Demonstration," in *Proc. ACM SIGPLAN Conf. Programming Language Design and Implementation (PLDI)*, 2022.
|
|
389
|
+
|
|
390
|
+
[26] Modal Labs, "Fast, lazy container loading," *modal.com/blog*, 2024. [Online]. Available: https://modal.com/blog/jono-containers-talk
|
|
391
|
+
|
|
392
|
+
[27] Google, "gVisor: Container Runtime Sandbox," 2024. [Online]. Available: https://gvisor.dev/
|
|
393
|
+
|
|
394
|
+
[28] Acuity Scheduling, "Embeds and Dynamic Links," *Acuity Scheduling Developers*, 2026. [Online]. Available: https://developers.acuityscheduling.com/docs/embedding
|
|
395
|
+
|
|
396
|
+
[29] Smashing Magazine, "Reliably Detecting Third-Party Cookie Blocking In 2025," May 2025. [Online]. Available: https://www.smashingmagazine.com/2025/05/reliably-detecting-third-party-cookie-blocking-2025/
|
|
397
|
+
|
|
398
|
+
[30] Qrvey, "2026 Iframe Security Risks and 10 Ways to Secure Them," 2026. [Online]. Available: https://qrvey.com/blog/iframe-security/
|
|
399
|
+
|
|
400
|
+
[31] Cloudbeds, "iFrame Deprecation: Upgrade to Cloudbeds Booking Engine Plus," 2025. [Online]. Available: https://myfrontdesk.cloudbeds.com/hc/en-us/articles/42963882806299
|