Unified provider architecture#877
Merged
Merged
Conversation
added 20 commits
May 30, 2026 14:03
…chitecture Collapse the three product entry points onto a single composition front-door, ExecutorApp.make, so each app is one legible file that differs only by the Layers it injects. Shared provider seams (identity, account, db, engine, mcp, plugins, errorCapture) live in the core/host packages; app-only surface (routes, services like billing) is passed as extensions the core never names. - ExecutorApp.make facade in @executor-js/api/server; cloud/self-host/local each express their whole scenario as one make() call. - IdentityProvider is one neutral seam with WorkOS and Better Auth as two impls (no forked tag, no dead placeholder); failures map to a shared Unauthorized | NoOrganization | Unavailable set. - Cloud MCP dissolved into one mcp/ folder (auth, telemetry, oauth-metadata, jwt, mount, session-durable-object); no MCP files at src/ root. - Self-host and local composition roots flattened to named providers; production builders are unconditional (test apps use a test helper). - libSQL replaces bun:sqlite/better-sqlite3 for self-host/local/sdk-test; per-connection PRAGMAs; root catalog declares @libsql/client + kysely-libsql. - One noun per concept (Principal), org/organization normalized, filenames match exports; billing (Autumn) no longer appears in any @executor-js/* package. - Self-host container: multi-stage Dockerfile + .dockerignore, serves the built SPA + API + /mcp + /api/auth under one Bun process.
Give all three apps one shared skeleton so structure matches how clean the composition already is: app.ts (the ExecutorApp.make root) + entry + barrel at src root, seam folders (auth/, db/, mcp/, account/), plugins.ts, observability, and testing/ for test scaffolding. - local: un-nest the whole app out of src/server/ up to src/, and group the libSQL/migration cluster into db/ (matching self-host's db/). - cloud: dissolve the api/ + services/ double grab-bag. Provider impls move to their seams (db/, engine/, mcp/, auth/, observability/); the billing extension to extensions/billing/; PluginsProvider to plugins.ts; api/ now holds only the typed-API composition glue (router, protected, layers, core-shared-services, error-response). Root loosies move home: jwks-cache to auth/, observability.ts to observability/, test scaffolding to testing/. - Point the executor-schema lint allowlist, wrangler test-worker entry, and miniflare unstable_dev path at the new locations. Behavior-neutral: format, lint, typecheck (36/36), and the local, self-host, and cloud (workerd + node + miniflare e2e) suites all pass.
Checkpoint of the provider-unification work. apps/host-cloudflare (new): Executor as one Cloudflare Worker — the 4th app on ExecutorApp.make. Cloudflare Access identity (+ Managed OAuth for MCP), D1 store, in-Worker QuickJS, the shared multiplayer SPA via Workers Static Assets, a minimal account provider, the MCP serving envelope, an idempotent deploy script, and an R2 large-value offload for the D1 handle. D1 compatibility: - fumadb drizzle adapter gains an interactiveTransactions flag (default true); D1 opts out (it rejects BEGIN/COMMIT) and runs transaction callbacks directly. libSQL/Postgres unchanged. - createExecutorFumaDb threads the flag through. - host-cloudflare wraps the D1 binding to offload oversized values (>~800KB) to R2 with a pointer in the row, rehydrating on read (D1 caps a value at ~1-2MB). Shared MCP: extract the in-process session store to @executor-js/host-mcp/in-memory-session-store; self-host and Cloudflare both consume it (only the per-session buildServer differs). self-host: single-tenant invite auth, zero-config first run, admin + system endpoints, docker-compose + docs. openapi: TODO documenting the proper blob-seam fix for large specs. local: correct the drizzle migrations path.
Multi-row inserts bind rows*columns parameters in one statement. Engines that cap bound parameters per query (Cloudflare D1: 100) overflowed with "too many SQL variables" on wide tables (e.g. deriving 300+ tools from a large OpenAPI spec). The drizzle adapter gains a maxBoundParameters option (threaded through createExecutorFumaDb and every nested transaction-scoped fromDrizzle); when set, createMany sizes batches so rows*columns stays within it. Unset for libSQL/Postgres (no tight cap). host-cloudflare sets 100. With this plus the R2 large-value offload and the no-interactive-transactions path, a 7MB OpenAPI spec (Vercel, 309 tools) now adds cleanly on D1.
- fumadb prisma adapter: recurse NOT conditions (`buildWhere(condition.item)`) instead of embedding the raw condition — `b.not(...)` produced invalid queries. - executor secretsStatus: skip connection-owned rows (as secretsList does) rather than short-circuiting to "missing"; a co-existing org-default value now resolves the secret. - host-cloudflare R2 offload: fail loud on a lost blob (was silently returning the pointer string, corrupting the read); add bounded retry with backoff around R2 put/get; simplify the D1 wrapper to a Proxy (removes the enumerated double-casts, delegates batch/exec/dump/withSession + internals). Verified: encrypted-secrets 7/7, sdk executor 17/17, fumadb 32, and the Vercel OpenAPI add (309 tools, R2 round-trip) still pass.
The per-session engine builder and the console error reporter were duplicated between apps/host-selfhost and apps/host-cloudflare. Extract both into @executor-js/api/server (where makeExecutionStack already lives): - makeMcpBuildServer(executionStackLayer): the makeExecutionStack → engine → createExecutorMcpServer chain; hosts pass only their fully-provided stack layer. - makeConsoleMcpErrorReporter(errorCapture): the McpErrorReporter-over-ErrorCapture seam; hosts pass only their capture layer. Both session-store.ts files collapse to a thin composition. The store body already lived in @executor-js/host-mcp/in-memory-session-store. Self-host MCP tests (8/8) still pass.
First tests for the Cloudflare host (previously zero coverage). vitest config + deps, and r2-blob-offload.test.ts covering the D1->R2 large-value offload: oversized value -> short pointer + one R2 blob; small values stay inline; pointer rehydrates to the original on read; and a lost blob fails loud (locks in the missing-blob fix). Also makes the pointer sentinel an explicit NUL-byte prefix.
Dockerfile: the runtime image shipped the entire build toolchain because it copied the full workspace node_modules. Reinstall production-only deps after the SPA build (--production --ignore-scripts; the root prepare hook is a dev tool) so the build/dev toolchain pulled by the full workspace install is dropped — vite, turbo, wrangler -> miniflare -> sharp/libvips (~800MB), astro, vitest. The built SPA in apps/host-selfhost/dist survives. Image 4.29GB -> 2.86GB; still boots healthy (health/SPA/MCP verified). serve.ts: wrap the entry-point startServer() so a pre-runtime failure (config / DB open, before Effect's runtime takes over) logs a diagnosable message and exits non-zero instead of an opaque unhandled rejection — readable container logs.
Boots the real worker via wrangler unstable_dev with a local D1 + R2 and dev-auth, then drives the HTTP surface: execute TypeScript (QuickJS-WASM on workerd), add an OpenAPI source + read it back (D1 write/read), the auth gate, and an MCP initialize handshake. First end-to-end coverage of the CF-specific stack together — locks in the D1 transaction/param-batch/R2-offload fixes and the QuickJS + MCP flows. 4/4 pass.
- serve.ts: keep the startup error format on one line so the boundary oxlint-disable targets it after oxfmt's wrapping. - format the host-cloudflare package.json/README touched by tooling.
Extend the MCP test to the full flow — initialize → notifications/initialized → tools/call execute → assert the QuickJS result (42). Real tool invocation through the MCP envelope + session store on workerd, not just the handshake.
Add a large synthetic OpenAPI spec (~1MB, 250 ops) to the e2e: the stored blob exceeds the ~800KB R2 offload threshold and the 250 derived tools exceed D1's 100 bound-parameter createMany limit. Asserts toolCount === 250 and reads the source back through R2 rehydration — the real-worker regression for all three D1 fixes (transactions, R2 offload, param batching). 5/5 pass.
Cloudflare Access service tokens (machine / API-key auth via the CF-Access-Client-Id/-Secret headers) carry `common_name` (the token's client id) instead of email/sub, so the verifier mapped them to an empty principal. Fall back to common_name for accountId/name. Extract the claim→Principal mapping into a pure, exported `principalFromAccessClaims` and unit-test it (human, admin, service-token, default-member). 4/4.
…itives Cloud and host-cloudflare are both Cloudflare Workers; their DO-backed MCP machinery should be one shared package differing only by injected deps. Scaffold @executor-js/cloudflare (packages/hosts/cloudflare) and move the verbatim-generic primitives into it: worker-transport.ts (the agents/mcp worker transport) and do-headers.ts (W3C identity/trace header helpers). Cloud re-imports them; its worker-transport test (6/6) runs unchanged via the workers pool. host-mcp stays neutral (no Cloudflare Workers SDK on the self-host Bun install).
The response peeker (telemetry span annotation + JSON-RPC body shape detection) is platform-generic; only its @sentry/cloudflare capture of -32603 internal errors was cloud-specific. Move it into the shared package and replace the hardcoded Sentry call with an injected onInternalError seam (cloud passes the Sentry capture; host-cloudflare will pass console/none). jsonRpcWebResponse was just host-mcp's jsonRpcErrorBody, so responses.ts stays in cloud. Cloud full suite green (57 workers + 105 node).
makeDurableObjectMcpSessionStore({getStub,newStub,onInternalError?}) owns the
generic worker→DO orchestration (create via newStub, forward via getStub by
session-id==DO-id, dispose; identity-header stamping, W3C trace propagation,
response peek, verbatim DO error passthrough). Cloud's cloudMcpSessionStoreLayer
collapses to a thin wrapper passing env.MCP_SESSION accessors + the Sentry
reporter. Cloud full suite green (57 workers + 105 node) — the 403 -32003 /
404 -32001 error bytes preserved.
The 900-line cloud MCP session Durable Object splits into a platform-generic base (cold restore, inactivity alarm, owner validation, transport upgrade, per-request span bridge, browser-approval store) and a thin cloud subclass that binds only its injected dependencies via five seams: openSessionDb, resolveSessionMeta, buildMcpServer, and the optional withTelemetry / captureCause hooks. host-cloudflare will bind the same base to D1.
Replaces the in-process MCP session store with the same Durable-Object-backed
store cloud uses (@executor-js/cloudflare), so a session lives in one
addressable isolate (DO id == session id). The in-memory map was invisible to
the next Worker isolate, so tools/list after initialize failed in production
("Not connected"); the DO routes every follow-up request back to the same
isolate.
host-cloudflare binds the shared McpSessionDOBase to its own seams: a long-lived
D1 handle, single-tenant session meta, and the QuickJS execution stack. Adds the
MCP_SESSION binding + new_sqlite_classes migration, exports the DO at the worker
entry, and asserts tools/list survives a fresh session in the workerd e2e.
The base's openSessionDb seam is now async-tolerant for D1's schema bring-up.
Both Cloudflare hosts cast `env.MCP_SESSION.get()` to the McpSessionDOStub RPC surface. Route it through an `unknown`-param helper so it is a single cast (no double cast through worker-types' DurableObjectStub), dropping the lint suppressions and the formatter-fragile disable comments.
Hosts that can't know their public URL at boot (a Worker has no static URL var) no longer need to hardcode it. New optional RequestWebOrigin seam carries the request's real origin; makeScopedExecutor resolves the effective webBaseUrl as configured-value-then-request-origin, read via Effect.serviceOption so it never enters the executor's R channel (CLI/tests just fall through). The shared execution-stack middleware provides it per HTTP request; the MCP session DO threads the create request's origin through McpSessionInit -> SessionMeta and the base provides it around buildMcpServer. We read request.url, not a spoofable X-Forwarded-Host, so the OAuth-callback URL stays trustworthy. host-cloudflare drops its hardcoded https://localhost default and the wrangler VITE_PUBLIC_SITE_URL placeholder, so a fresh self-host deploy gets correct secret/OAuth handoff links with zero config; setting the var still overrides.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-marketing | bb1c98a | Commit Preview URL Branch Preview URL |
Jun 02 2026, 02:04 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-cloud | bb1c98a | Jun 02 2026, 02:04 AM |
added 2 commits
May 31, 2026 13:59
Integrates 8 main commits (connection identity, typed plugin storage, Google source flow, OpenAPI OAuth reuse) onto the refactor branch. Conflict resolutions: - Connection API: reverted the branch's cosmetic setIdentityLabel->setConnectionLabel rename back to main's setIdentityLabel and kept main's added setIdentityOverride (load-bearing across the API handlers + UI). Same identity_label column. - Schema/db files the branch MOVED (services/->db/ for cloud schema, server/->db/ for local libSQL): kept the moves, ported main's changes to the new paths — regenerated the cloud drizzle schema (now carries identity_override) and ported main's defensive identity_override ALTER into the libSQL bring-up. - Re-exposed two symbols the branch had internalized that main's new code needs: OAUTH2_DEFAULT_TIMEOUT_MS + assertSupportedOAuthEndpointUrl via /host-internal (host connection-identity handler), and buildToolTypeScriptPreview on the barrel (openapi Google-discovery suite asserts tool previews). Gates green: typecheck 38/38, lint 0/0, format clean; sdk/api/execution/openapi/ workos-vault + cloud (58+106) + local + both hosts all pass.
The unify commit committed apps/host-selfhost/.executor-dev/ — the per-app dev runtime dir holding a generated secret.key and the local dev SQLite DB. The gitignore only covered apps/local/.executor-dev/. Untrack it and replace the app-specific rule with a global .executor-dev/ so no host commits dev state.
2c602ae to
fe379bf
Compare
added 4 commits
May 31, 2026 15:31
The refactor mounts the Autumn billing proxy at /extensions/billing/route/* and Swagger UI at /extensions/docs, but start.ts's dispatch only forwarded /api/* and MCP paths to the app handler — so every authenticated billing call (the React app posts to /extensions/billing/route/* via <AutumnProvider>) and /extensions/docs fell through to TanStack Start and 404'd. Extract the app-owned path decision into ./app-paths (isAppOwnedPath) so it is unit-testable without building the whole app, add /extensions/* to it, and add a regression test pinning every app-owned surface (incl. the billing proxy + docs) and the Start-owned routes. Caught by review; no test covered the dispatch.
Boots the actual cloudApiHandler (ExecutorApp.make's toWebHandler — the handler start.ts forwards to) in the workers pool and drives it with raw Requests, asserting each served surface is REACHED, not 404'd into the SPA fallback: the Autumn billing proxy (401 JSON, the exact regression), Swagger UI, the OpenAPI spec, and the protected /api auth gate. This is the integration complement to app-paths.test.ts, which guards the start.ts dispatch decision. Together they cover both halves of the billing-404 class: does start.ts forward /extensions/*, and does the handler serve it. Runs in the workers pool because the composed app imports agents/mcp (workerd only); the asserted surfaces short-circuit before any WorkOS/Autumn network.
The refactor mounted the Autumn billing proxy at /extensions/billing/route/* and
Swagger at /extensions/docs — leaking the internal 'extensions' DI-seam name into
the public URL, churning the stable /api/autumn + /api/docs paths, and creating a
new top-level dispatch namespace (the source of the billing-404).
Keep the extensions.routes SEAM (host-specific routes injected via ExecutorApp.make)
but serve them under /api like everything else:
- /extensions/billing/route/* -> /api/billing/* (also drops the 'autumn' vendor name)
- /extensions/docs -> /api/docs
AutumnProvider pathPrefix follows; the legacy CloudDocsLive paths are aligned.
This deletes the /extensions dispatch special-case entirely — start.ts/app-paths
go back to /api + MCP, so the unreachable-surface class can't recur. The file
comment already documented the intent ('all serve UNDER the /api prefix'); the
/extensions paths had contradicted it.
Verified: cloud 78 workers + 106 node, reachability test confirms /api/billing/*
coexists with the /api-prefixed typed router (401 JSON, not 404).
…sets - cloud: instantiate the unified app handler lazily inside the .server() callback so TanStack Start strips it (and ./app -> telemetry -> cloudflare:workers) from the CLIENT bundle. A module-top-level cloudApiHandler() leaked the workerd-only cloudflare:workers virtual module into the browser build and broke it. - cli + desktop: point the embedded-drizzle-migrations generators at src/db/embedded-migrations.gen.ts (the post-move location the runtime imports), not the deleted src/server/ path which ENOENT'd in CI. - local: update drizzle.config schema path to src/db/executor-schema.ts. - host-cloudflare e2e: ensure a minimal ./dist exists before unstable_dev so its assets-dir validation passes on a fresh CI checkout (no vite build).
The local server's SQLite driver moved from bun:sqlite to libSQL, whose
native addon `@libsql/<platform>` isn't bundled by `bun build --compile`
(same bunfs limitation as @napi-rs/keyring), so the compiled binary's
smoke test (`executor --version`) crashed with
"Cannot find module '@libsql/<platform>'". This was masked until the
embedded-migrations path fix let the build reach the smoke test.
- build.ts: copy the platform `@libsql/<target>/index.node` next to the
executable as `libsql.node` (mirrors the keyring.node colocation).
- patch libsql@0.5.29 so its loader honors EXECUTOR_LIBSQL_NATIVE_PATH and
loads the colocated binding directly (bun resolves the bare
`require('@libsql/<plat>')` natively, bypassing any JS resolver hook).
- native-bindings.ts: a side-effect module imported FIRST in main.ts that
publishes the env var before the @executor-js/local -> libSQL graph loads
it eagerly (ESM evaluates imports before the importer body, so setting it
in main.ts's body ran too late). keyring colocation moved here too.
Verified locally: build:preview:tarball smoke test passes; the binary opens
the local store without the native-module crash.
@executor-js/cli
@executor-js/config
@executor-js/execution
@executor-js/sdk
@executor-js/codemode-core
@executor-js/runtime-quickjs
@executor-js/plugin-file-secrets
@executor-js/plugin-graphql
@executor-js/plugin-keychain
@executor-js/plugin-mcp
@executor-js/plugin-onepassword
@executor-js/plugin-openapi
executor
commit: |
added 6 commits
June 1, 2026 15:16
Resolves the conflict with #878 (org-scoped MCP routes), which added URL-pinned org selection to the monolithic apps/cloud/src/mcp.ts this branch had already split into the shared @executor-js/host-mcp envelope + cloud seams. The feature is preserved, re-implemented cloud-only on the new architecture (the shared envelope stays org-agnostic so self-host/CF are untouched): - mcp/mount.ts: classifyMcpPath is now org-aware (recognizes /org_xxx/mcp and the org-scoped protected-resource doc; only org_-shaped segments claimed) and a new prepareMcpOrgScope rewrites an org-scoped path to the bare path the envelope routes, carrying the URL-pinned org in an internal header. - mcp/auth.ts: MCP_ORGANIZATION_HEADER + reader, org-scoped resource/metadata URL builders, org-aware bearerChallengeFor. - mcp/auth-provider.ts: resourceMetadataUrl, the discovery doc, finishAuthorized, and the 401 challenge all take the org from the header (falling back to the token claim); membership is still re-checked per request, so the URL is a selector, not a trust boundary. - start.ts + testing/test-worker.ts: apply prepareMcpOrgScope at the dispatch boundary; old mcp.ts deleted. Verified: typecheck 38/38, lint clean, mcp-flow 22/22 (incl. the org-scoped routing + membership-denial cases), cloud node MCP 24/24, reachability 4/4, react mcp-install-card 11/11.
…ervices Experiment outcome: the 'ugliness' of api/core-shared-services.ts traced to a SINGLE import — handlers.ts's `setCookie`/`deleteCookie` from @tanstack/react-start/server, the only thing pulling Start into the layers.ts -> Durable Object graph. core-shared-services.ts existed solely to firewall that import out of the DO/test-worker bundle. Root fix (removes the coupling instead of just containing it): - handlers.ts: the 6 wos-session cookie ops drop the react-start import. logout (.handleRaw) uses the existing Effect deleteResponseCookie; the 5 typed .handle() handlers (switchOrganization/createOrganization/acceptInvitation) queue writes on a new request-scoped Session.cookies, applied to the response in SessionAuthLive via the SAME RESPONSE_COOKIE_OPTIONS/DELETE_COOKIE_OPTIONS constants, so the Set-Cookie bytes are identical. - middleware.ts: Session gains a cookies: SessionCookieWriter (no-op default for the account/SPA path); SessionCookieOptions is derived from setCookieUnsafe so it can't drift. With handlers.ts react-start-free, the layers.ts chain is too, so the firewall is unnecessary: core-shared-services.ts is DELETED and its CoreSharedServices alias moves beside WorkOSClient in auth/workos.ts (a focused service root the DO imports instead of the whole HTTP API). Bonus: the cloud API no longer depends on Start's ambient server context for cookies (runtime-agnostic direction). Verified: typecheck 38/38, lint+format clean, cloud node 106/106 (incl. the miniflare e2e DO bundling + the exact Set-Cookie assertions) and workers 82/82.
…o-op) Follow-up to the react-start removal: the cookie writer was a field on the Session data type, which forced every Session producer that never writes a cookie (OrgAuth, the account API, test fixtures) to supply a meaningless noopCookieWriter. That null-object was the tell that a write capability had been bolted onto a data shape. Make it a proper request-scoped service instead: SessionCookies. SessionAuth now declares `provides: SessionContext | SessionCookies` (a security middleware can provide a union — the handler R is Exclude<R, Provides>), and SessionAuthLive provides both then drains the queue onto the response. The 5 typed handlers `yield* SessionCookies` only where they actually refresh the cookie. Session reverts to pure data; sessionFromSealed loses its cookies param. Net: no production no-op anywhere (only an inline test-fixture stub in the test layer, which is normal). Verified: typecheck 38/38, lint+format clean, cloud node 106/106 (incl. exact Set-Cookie assertions + the miniflare e2e) and workers 82/82.
The glob .executor-dev/ missed the self-host data dir .executor-selfhost/, so apps/host-selfhost/.executor-selfhost/secret.key (a generated dev session key) got committed in 8154719. Broaden to .executor-*/ to cover every per-app data dir, add a belt-and-suspenders secret.key rule, and untrack the leaked key (it stays on disk, regenerated on next boot).
Plugins persist through host-owned facades (pluginStorage/blobs), not their own tables, so collectTables ignored its plugins arg and returned the fixed coreSchema. Drop the dead coupling: - collectTables() takes no args; update all call sites (runtime, hosts, tests). - The DB handles (d1.ts, self-host-db.ts) no longer take/thread plugins; the CF Durable Object sheds its cfPlugins field entirely. - `schema generate` no longer loads executor.config.ts — the table set is fixed and host-independent, so the command needs only --namespace/--adapter/--provider (dropped --config; cloud db:schema script updated). - Fix stale comments claiming plugins feed the FumaDB schema. Behavior-preserving: the generated schema is byte-identical (table content unchanged; verified by regen). typecheck 38/38, lint+format clean, scope-policy tests pass.
Boots the cloud Vite dev server (stub env + throwaway PGlite, no secrets) and
drives it in a real browser to assert the TanStack Start client entry loads and
the SPA hydrates. This failure mode is invisible to the request-level Vitest
suites: every module serves a clean 200 to a request, so the break only surfaces
in a browser running the module graph.
The harness warms up once to absorb Vite's benign one-time cold-start dependency
re-optimization (which force-reloads and transiently aborts the in-flight client
entry), then asserts the settled load is clean: no dynamic-import failures, no
aborted Vite module-graph requests, and the router mounted on hydration.
- apps/cloud/e2e/{client-entry-hydration.spec.ts,e2e-server.ts} + playwright.config.ts
- test:e2e script and playwright dev deps
- dev-db.ts: env-overridable port/path so the e2e DB runs alongside `bun dev`
- oxlint override for e2e specs (browser-driving APIs the Effect rules forbid)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
self hosting & deploy to cloudflare support