An open-source, embeddable, multi-tenant booking widget. Drop a booking calendar
into any website — Squarespace, Wix, or plain HTML — with a single <script> tag. Built on
Cloudflare Workers (Hono + React) with isolated per-tenant configuration, availability and
capacity rules, and pricing.
Status — early release (v0.1). The booking flow stores requests in D1 (Cloudflare's SQLite). Google Calendar OAuth (Phase 1) and email + invite-only customers (Phase 2) are now implemented. See Roadmap.
- One-tag embed — an auto-resizing iframe with an origin- and source-validated
postMessagechannel; no third-party JS on the host page. - Multi-tenant — each provider is a tenant resolved from the URL, with isolated config, services, pricing, accepted pet types, and bookings.
- Capacity-aware availability — per-tenant, per-day capacity and conflict rules (boarding cap, house-sit cap, max stay length, and business timezone — each nullable for unlimited / instance-default), plus blocked dates, computed from a single source of truth.
- Two auth flows — passwordless email-code sessions for customers; password + JWT sessions for the tenant admin dashboard.
- Zero runtime dependencies in the booking/date/pricing core (pure TypeScript).
<script src="https://<your-worker>/embed.js" data-pawbook-tenant="your-slug"></script>The loader injects an auto-resizing iframe, validates every postMessage by origin and
source, and re-dispatches a pawbook:booked DOM event on the host page so you can react to
completed bookings. Script-stripping hosts (e.g. Wix "Embed a site") can use the plain-iframe
variant shown in the admin dashboard.
Host site ──<script>──▶ /embed.js ──iframe──▶ /embed/:slug (React widget)
│
▼
Hono Worker ──▶ D1 (per-tenant config + bookings)
└─▶ KV (caches, login codes)
The Worker serves the built widget/admin assets and an API under /api/:slug/*. Tenancy is
resolved from the slug; capacity and pricing rules live in src/shared/ (pure functions).
npm install
npm run seed:local # schema + demo tenants (Sunny Paws, Happy Tails, Paws & Relax)
printf 'TOKEN_SECRET=%s\nENVIRONMENT=development\n' "$(openssl rand -base64 32)" > .dev.vars
npm run dev # builds the widget + runs wrangler dev- Demo page (two tenants side by side):
http://localhost:8787/demo - Widget:
http://localhost:8787/embed/sunny-paws - Admin dashboard:
http://localhost:8787/admin
npx wrangler d1 create pawbook-db # put database_id into wrangler.jsonc
npx wrangler kv namespace create PAWBOOK_CACHE # put id into wrangler.jsonc
npx wrangler secret put TOKEN_SECRET # a strong random value (openssl rand -base64 32)
npx wrangler secret put GOOGLE_CLIENT_ID # OAuth 2.0 client ID from Google Cloud Console
npx wrangler secret put GOOGLE_CLIENT_SECRET # OAuth 2.0 client secret from Google Cloud Console
npx wrangler secret put GOOGLE_OAUTH_REDIRECT_URI # absolute callback URL, e.g. https://<your-worker>/oauth/google/callback — must EXACTLY match the authorized redirect URI registered in Google Cloud
npm run deploy
npm run seed:remote # ⚠️ demo tenants/logins — do NOT run against a real prod DBnpm run deploy ships worker code only — it does not touch the database. A fresh install
gets the full, current schema from sql/schema.sql (via seed:remote). But when you upgrade an
already-provisioned database, you must apply any new files in migrations/ yourself, or the new
code will query columns that don't exist yet and every /api/:slug/* route will 500.
Apply pending migrations against the live DB (each file is idempotent-by-intent; run the ones added since your last deploy):
npx wrangler d1 execute pawbook-db --remote --file ./migrations/0002_tenant_config_limits.sql
npx wrangler d1 execute pawbook-db --remote --file ./migrations/0003_calendar_oauth_and_invites.sqlUse --local instead of --remote for your dev database. (Fresh installs created from
sql/schema.sql already include every column and do not need to replay migration files.)
In local development (ENVIRONMENT=development) login codes are shown on screen. Production
emails them and fails closed if no provider is configured — /identify returns 503 rather than
ever leaking a code. Set two secrets to enable email:
npx wrangler secret put RESEND_API_KEY # from https://resend.com (free tier)
npx wrangler secret put RESEND_FROM # e.g. "Pawbook <bookings@yourdomain.com>" (verified sender)When RESEND_API_KEY is set, the /identify response no longer returns the code — it is emailed.
npm test # Vitest, backed by in-memory SQLite (node:sqlite)
npm run typecheck # wrangler types + tsc -b
npm run lint # ESLint
npm run format # Prettier checkapp/ React apps — embed widget, admin dashboard, shared UI/API client
server/ Hono Worker — routes, auth/token, tenant resolution, availability, db
src/shared/ Pure booking/date/pricing logic (zero runtime dependencies)
sql/ D1 schema + demo seed
public/ embed.js loader + demo host
Implemented
- Phase 1 — Google Calendar OAuth. Per-tenant "Connect Calendar" so bookings create real calendar events.
- Phase 2 — Email + invite-only customers. Real email delivery (login codes, invites) and a provider-managed customer list.
Upcoming
- Phase 3 — Self-serve tenant signup, custom domains, billing.
Contributions are welcome — see CONTRIBUTING.md. By participating you agree to the Code of Conduct. To report a security issue, see SECURITY.md.
MIT © 2026 Brad Burch