Self-hosting Call Embed

Deploy the application as a Cloudflare Worker backed by PlanetScale Postgres and accounts with Ably and Daily. Each deployment serves a single organization.

Prerequisites

1. Clone the repository

Clone the repo and install the workspace dependencies.

git clone https://github.com/lukesteinbicker/embed.git
cd embed
pnpm install

2. Configure your environment

Copy the Wrangler development secret template. At a minimum set BETTER_AUTH_SECRET, ABLY_API_KEY, DAILY_API_KEY, and DAILY_DOMAIN when video is enabled. For chat-only hosting, omit both Daily variables. Organization owners and admins can toggle calls in Settings → Organization. Claiming, chat, visit notes, and completion remain available in the dashboard.

cp apps/dashboard/.dev.vars.example apps/dashboard/.dev.vars
openssl rand -base64 32   # paste into BETTER_AUTH_SECRET

3. Configure Hyperdrive and deploy

Create a Hyperdrive configuration from the PlanetScale direct URL, put its returned ID in apps/dashboard/wrangler.jsonc, run migrations, then deploy the Worker.

pnpm --filter @app/dashboard exec wrangler hyperdrive create embed-db --connection-string="YOUR_PLANETSCALE_DIRECT_URL"
DATABASE_DIRECT_URL="YOUR_PLANETSCALE_DIRECT_URL" pnpm db:migrate
pnpm deploy

4. Create the first account

The first user to sign up becomes the administrator. After that, sign-ups are invite-only. Invite teammates from the dashboard, or set AUTH_STAFF_DOMAIN so anyone at your company domain can self-register.

5. Embed the widget

Create a website installation from the dashboard's Install page, then add its script to the customer site. The bundle is served from jsDelivr; point data-api at your deployment so the widget talks to your instance.

<script
  src="https://cdn.jsdelivr.net/npm/@lukesteinbicker/callembed@0.1.14/dist/embed.iife.js"
  data-token="YOUR_EMBED_TOKEN"
  data-api="https://your-app.example.com"
  defer
></script>

Built-in rate limiting

The public widget endpoints (visit init, status updates, and realtime tokens) are unauthenticated, so a busy or hostile page can generate a lot of requests. The Worker's PUBLIC_RATE_LIMITER binding protects /api/contact/*. See Cloudflare's documentation.

Full documentation lives in the repository.