| Name | Last commit date | |
|---|---|---|
| .. | ||
| scripts | ||
| src | ||
| web | ||
| .gitignore | ||
| CHANGELOG.md | ||
| executor.config.ts | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| tsr.routes.ts | ||
| vite.config.ts | ||
| vitest.config.ts | ||
| wrangler.jsonc | ||
@executor-js/host-cloudflare
Executor as a single Cloudflare Worker. The fourth app on the shared
ExecutorApp.make facade (alongside cloud, self-host, and local) — same code
paths, different injected providers:
| Seam | Cloudflare provider |
|---|---|
| identity | Cloudflare Access JWT (Cf-Access-Jwt-Assertion) — no app login |
| db | D1 (SQLite) via the shared FumaDB assembly |
| engine | QuickJS-WASM, in-Worker (no extra binding) |
| mcp | Access-JWT auth + the shared in-process session store |
| account | /account/me from the Access principal (members/keys → Access) |
| web | the shared multiplayer SPA (Workers Static Assets) |
Single-tenant: every Access-verified principal belongs to the one configured org. Members and credentials are managed in Cloudflare Access, not in-app.
Surfaces
GET /— the shared Executor web UI (Sources, Connections, Secrets, Policies) — the same shell as cloud/self-host, built byvite buildintodist/and served via Workers Static Assets (single-page-applicationfallback for client routes)./api/*— the full Executor API (scopes, sources, secrets, account, …)./mcp— streamable-HTTP MCP with anexecutetool.
run_worker_first in wrangler.jsonc keeps /api/* + /mcp on the Worker;
everything else is the SPA. Every API/MCP route is gated by the Access JWT (401
without). The SPA's auth context reads /api/account/me.
Deploy
bunx wrangler login
bun run deploy:setup # apps/host-cloudflare — provisions D1 + secret + deploys
deploy:setup (scripts/deploy.sh) is idempotent. It creates or reuses the
executor D1 database, writes its id into wrangler.jsonc, generates and
uploads EXECUTOR_SECRET_KEY, then deploys. It then prints the one manual step.
The one manual step — Cloudflare Access
After the first deploy, API and MCP requests return 503 and name the missing Access variables until configuration is complete. In the Zero Trust dashboard:
- Access → Applications → Add an application → Self-hosted
- Application domain:
executor-cloudflare.<your-subdomain>.workers.dev - Add an Access policy (e.g. Emails ending in
@yourcompany.com) - Copy the Application Audience (AUD) tag, then:
bunx wrangler deploy \ --var ACCESS_AUD:<aud> \ --var ACCESS_TEAM_DOMAIN:<your-team>.cloudflareaccess.com \ --var ADMIN_EMAILS:<admin@example.com>
Now visiting the Worker prompts an Access login; the Worker validates the issued
JWT on every request. Unauthenticated requests return 401. MCP clients present
an Access JWT or Cf-Access-Client-Id/-Secret service-token headers.
The Access values are live Worker variables, not values in wrangler.jsonc.
Wrangler's keep_vars option preserves them during later code deploys. Run the
command above again whenever you need to change them.
Local development
# .dev.vars
EXECUTOR_SECRET_KEY=dev-secret-key-0123456789abcdef
ENABLE_DEV_AUTH=true # bypass Access; every request is a fixed dev admin
bun run build # vite build -> dist/ (the SPA)
bunx wrangler dev --local # serves the SPA + Worker API together
bun run dev:web runs the Vite dev server (HMR) for UI work; point its API at a
running wrangler dev if you need live data.
ENABLE_DEV_AUTH is a dev-only escape hatch — never set it in a deployed
environment (it disables the Access gate).
Notes
- The QuickJS engine WASM is vendored into
src/quickjs-engine.wasm(Workers forbid runtime WASM compilation; it must be statically imported). Refresh it after bumping the engine withbun run vendor-wasm. - MCP sessions live in-process (one isolate owns a session). The cross-isolate
upgrade is a Durable Object behind the same
McpSessionStoreseam. - When Cloudflare's dynamic Worker Loader leaves closed beta, the QuickJS code
substrate swaps for the dynamic-worker executor behind the
engineseam — a one-Layer change.