Embed the widget
One script tag, Shadow-DOM isolated, under 30 KB gzipped. Position-configurable, theme-configurable, host-page-CSS-immune. Cross-channel resume requires phone OTP — number-match alone is a spoofing vector.
Drop it in
The widget mounts itself on DOMContentLoaded, reads its config from data-* attributes on its own script tag, and attaches a Shadow DOM root to document.body. Host-page CSS cannot leak into the widget and the widget’s styles cannot leak out.
<script
src="https://foyer.philiprehberger.com/widget.js"
data-foyer-business-id="01J4YQX9MA0RBPV6N7K8WJ6XYZ"
data-foyer-position="bottom-right"
data-foyer-theme="auto"
async
></script>Configuration
| Attribute | Values | Default |
|---|---|---|
data-foyer-business-id | ULID — your business’s ID | required |
data-foyer-position | bottom-right, bottom-left | bottom-right |
data-foyer-theme | auto, light, dark | auto |
What the widget does
- POSTs to
/v1/web/sessionswith the business ID and gets back a short-lived session token bound to the visitor’s IP. - Shows a launcher button. On click, opens a chat panel and POSTs each customer message to
/v1/web/inboundwith a client-generatedwidget_message_id(UUIDv7) for idempotency. - Streams agent replies back. Server-sent events for the panel; fallback short-poll if SSE drops.
Cross-channel resume
If the visitor has texted the business before from this phone number, the web session can resume the SMS conversation — but only after OTP verification. The widget collects the phone, calls /v1/web/sessions/:id/verify-phone to issue a 6-digit code, the customer types it back, and only then does the web session bind to the SMS conversation. Phone-number match alone is a spoofing vector and is not accepted as resume auth.
Consent
If the visitor’s phone is STOP’d on this Twilio number, the widget receives a consent_blockedresponse on its first message and renders a static notice asking the visitor to contact the business directly. The widget will not resume a STOP’d SMS conversation, and will not attempt cross-channel outbound.
CSS isolation guarantees
Foyer’s widget uses an open Shadow DOM root so a debugger can still inspect the tree, but no host-page !important selector will reach inside. The widget ships its own minimal CSS reset and renders all interactive surfaces inside the shadow root. Tested against Webflow, Squarespace, and Wix sandbox themes — none of which could break it.
Bundle size
The widget is hard-capped at 30 KB gzipped. A CI gate runs the gzipped size of dist/foyer-widget.js against the cap on every PR; the build fails if the cap is exceeded. The current floor is well under that — Preact plus the chat panel, OTP prompt, and transport are the only payload.
See the widget’s own README in the repo for the local dev setup.