wisewigdocs

Cloudflare reference deployment

Self-hosted deployment profiles

The minimum production topology is two Workers: the Astro site and tenant workspace. The plugin sandbox, public MCP, and private MCP are optional Workers, not prerequisites for a basic marketing site. The CLI groups them into core (2), plugins (3), ai (4), and full (5) profiles; the AI profile deliberately keeps the physically separated public/private MCP pair together:

Executable EmDash plugins use Cloudflare Dynamic Workers, a separate product from Workers for Platforms. Dynamic Workers require the ordinary Workers Paid plan. This does not move tenant/site multitenancy back onto Workers for Platforms; that remains managed-isolated v2. core and ai do not require the Worker Loader. A plugins or full deployment without an available loader may still run code-free declarative plugins, but executable sandboxed plugins are skipped and reported unavailable rather than downgraded to native execution. The isolated EmDash backend.js runner and bounded storage/content/media/HTTP bridge are implemented; deployed executable acceptance still requires a paid-plan Worker Loader.

pnpm wise-wig cloudflare plan --profile core --cwd examples/morning-bell-coffee
pnpm --filter @wise-wig/morning-bell-coffee build
pnpm wise-wig cloudflare prepare --profile core --cwd examples/morning-bell-coffee --deployment-config .wise-wig/cloudflare.local.json
pnpm wise-wig doctor --profile core --cwd examples/morning-bell-coffee
pnpm --filter @wise-wig/tenant-workspace-worker exec wrangler deploy --dry-run --config wrangler.wise-wig-core.jsonc
pnpm --dir examples/morning-bell-coffee exec wrangler deploy --dry-run --config dist/server/wrangler.wise-wig-core.json

After deploying the full profile, collect a read-only, redacted receipt from the exact clean commit:

pnpm wise-wig cloudflare self-hosted preflight \
  --cwd examples/morning-bell-coffee \
  --operator release-operator \
  --public-mcp-url https://mcp.customer.example/mcp

The preflight runs the full-profile doctor, pins Git HEAD, verifies Wrangler authentication, requires exactly one production version at 100% traffic for each of the five Workers, and compares that version’s binding names to fixed required/forbidden capability policies. It also performs a bounded anonymous MCP initialize. The customer site Worker name comes from the prepared Astro config. The mode is read-only and the private 0600 receipt contains no raw command output, account/resource/version identifiers, secrets, response bodies, or headers. Missing or split production deployments are blockers; missing PLUGIN_LOADER, cross-tenant bindings, managed-only bindings, and other capability mismatches are failures.

Preparation removes inactive optional service bindings from generated deployment configuration. This matters because a Cloudflare service binding targets a Worker already present on the same account; merely avoiding calls to a missing binding does not make that target optional. The managed hosting deployer remains separate operator infrastructure and is omitted from self-hosted profile output.

For a real deployment, put one non-secret descriptor at <site>/.wise-wig/cloudflare.local.json and keep it out of version control. Pass it with --deployment-config; WiseWig validates and hydrates all active Worker configs together. The descriptor contains resource identifiers, public OAuth endpoints and client IDs, public origins, plugin trust public keys, and an optional Analytics Engine dataset. It never contains SESSION_SECRET, OAUTH_CLIENT_SECRET, or PROVISIONING_SECRET; set those with wrangler secret put on the specific Worker. OAuth and authorization-state KV IDs must be distinct, all origins/endpoints must use HTTPS, and doctor treats replace-*, *.example, or pending-* values in prepared configs as errors.

{
  "schemaVersion": 1,
  "d1": { "databaseName": "wise-wig-production", "databaseId": "<D1 UUID>" },
  "kv": {
    "publicContentId": "<32 hex characters>",
    "oauthId": "<different 32 hex characters>",
    "authStateId": "<different 32 hex characters>"
  },
  "r2": { "mediaBucket": "wise-wig-media", "backupsBucket": "wise-wig-backups" },
  "queues": { "publish": "wise-wig-publish", "deadLetter": "wise-wig-publish-dlq" },
  "oauth": {
    "issuer": "https://identity.provider.tld",
    "authorizationEndpoint": "https://identity.provider.tld/oauth/authorize",
    "tokenEndpoint": "https://identity.provider.tld/oauth/token",
    "userinfoEndpoint": "https://identity.provider.tld/oauth/userinfo",
    "siteClientId": "site-editor-client-id",
    "privateMcpClientId": "private-mcp-client-id"
  },
  "siteOrigin": "https://www.customer.example",
  "privateMcpOrigin": "https://mcp.customer.example",
  "mediaPublicBaseUrl": "https://www.customer.example/_wisewig/media/",
  "pluginTrustKeys": []
}

Create two confidential OIDC applications even when both use the same identity provider. The site application permits exactly https://www.customer.example/api/auth/callback; the private MCP application permits exactly https://mcp.customer.example/auth/callback. Their client IDs, client secrets, redirect allowlists, sessions, and state stores must remain distinct. Plain GitHub OAuth is not compatible because WiseWig requires OIDC UserInfo with a stable sub; use an OIDC provider such as Google, Auth0, or Okta (GitHub can still be an upstream identity through an OIDC broker).

Preparation binds the site and private MCP Workers to the exact hostnames from siteOrigin and privateMcpOrigin as Cloudflare custom domains and disables their additional workers.dev endpoints. The validated descriptor accepts origins only, so it cannot smuggle a path, query, fragment, credentials, or wildcard into either route.

Analytics Engine is optional for self-hosted deployments. Omit analyticsEngineDataset to remove that binding; durable operational/audit records remain in D1. Add "analyticsEngineDataset": "wise_wig_operations" only after enabling Analytics Engine in the account. Dynamic Workers are different: removing the Worker Loader disables executable EmDash isolation, so plugins/full cannot pass the executable-plugin beta gate without Workers Paid.

The self-hosted Morning Bell reference is five framework Workers plus an optional operator-owned hosting deployer service:

  1. @wise-wig/tenant-workspace-worker exports TenantWorkspaceDO and has no public route. One deterministic SQLite-backed object coordinates each agency tenant; the Worker also consumes publish and dead-letter queues to refresh public indexes/caches and dispatch isolated after-publish hooks.
  2. @wise-wig/morning-bell-coffee is the Astro site Worker. It binds D1 as the routing/membership control plane, the remote tenant workspace namespace, R2 as MEDIA, and a required signed-session secret. Astro reserves ASSETS for built static files.
  3. @wise-wig/plugin-sandbox-worker is non-public and reachable only through the site’s PLUGIN_SANDBOX service binding. Its current storage binding is a digest-addressed artifact Durable Object. The beta runner adds a Worker Loader and narrow loopback capability bridge; it must not expose raw tenant D1/R2/Durable Object/session bindings to dynamically loaded code.
  4. @wise-wig/public-mcp-worker is an anonymous Streamable HTTP MCP service with only sanitized PUBLIC_CONTENT KV plus its own MCP/rate Durable Objects.
  5. @wise-wig/private-mcp-worker is the OAuth-protected MCP service. In the managed profile it binds identity D1 plus the production dispatch namespace, resolves the exact ready tenant/site mapping, and sends a permission- and OAuth-scope-pinned operation to that tenant’s user Worker. It has no cross-script tenant Durable Object binding and is never mounted in the public MCP process.

Create wise-wig-publish-events and wise-wig-publish-dlq, then deploy the workspace and plugin sandbox Workers first, apply the D1 migrations, configure secrets, and deploy the Astro/MCP Workers. The committed Wrangler configurations and generated types are checked during pnpm check; all Workers are bundled during pnpm build. A site deployment can be validated without external changes:

pnpm --filter @wise-wig/tenant-workspace-worker build
pnpm --filter @wise-wig/plugin-sandbox-worker build
pnpm --filter @wise-wig/morning-bell-coffee build
pnpm --filter @wise-wig/public-mcp-worker build
pnpm --filter @wise-wig/private-mcp-worker build
cd examples/morning-bell-coffee
pnpm exec wrangler deploy --dry-run --config dist/server/wrangler.json

Managed multi-tenant profile

The v2 managed-isolated profile adds @wise-wig/tenant-dispatch-worker, the non-public @wise-wig/hosting-deployer-worker, and one @wise-wig/tenant-user-worker script per tenant in a Workers for Platforms namespace. It is intentionally not a beta deployment option. The dispatch Worker resolves a verified hostname in D1 and invokes only the persisted ready script. It is also the sole consumer for every tenant publish Queue, retaining shared public KV, cache, analytics, and plugin-sandbox side effects in trusted platform code. Each user Worker owns a declaratively exported SQLite TenantWorkspaceDO, a unique R2 bucket for media and tenant-local backup prefixes, and a unique publish Queue, all named from the unique Worker identity. The separate operator deployer owns trusted release and ARCHIVES R2 buckets used to verify modules and retain checksum-verified tenant exports before destructive lifecycle work; neither bucket may be exposed to request-facing Workers or tenant scripts.

The tenant user-Worker template deliberately has no public route of its own. It accepts platform-owned dispatch context, pins that context to its deployment-time tenant ID, uses the constant local object name workspace, and exposes published structured data only to the public audience. Draft data requires a private audience derived by the dispatcher from a signed, revocable session plus D1 tenant/site membership. The dispatcher strips the platform session cookie before invoking tenant code. An operator-only deployer loads a trusted build artifact and uses CloudflareWorkersForPlatformsApi; after deployment it can initialize a complete typed site bundle through an operator dispatch and then verify the D1 hostname/identity mapping. Cloudflare account credentials never enter the dispatcher, Astro site, theme, editor, plugin sandbox, or MCP Workers.

The managed Astro Worker binds TENANT_DISPATCH as its sole WiseWig platform service. Its public SSR, OAuth proxies, forms, editor, and management APIs use TenantBrowserDispatchClient; the self-hosted profiles do the inverse and the CLI removes the managed-only service binding. Do not deploy a managed site with both data paths enabled: TENANT_DISPATCH is authoritative whenever present.

pnpm --filter @wise-wig/morning-bell-coffee build
pnpm wise-wig cloudflare prepare --managed-isolated --cwd examples/morning-bell-coffee
pnpm wise-wig doctor --managed-isolated --cwd examples/morning-bell-coffee
pnpm --dir examples/morning-bell-coffee exec wrangler deploy --dry-run --config dist/server/wrangler.wise-wig-managed.json
pnpm --filter @wise-wig/tenant-dispatch-worker build
pnpm --filter @wise-wig/tenant-user-worker build

The generated managed Astro configuration requires the exact TENANT_DISPATCH service and strips TENANT_WORKSPACE, shared DB, tenant/shared MEDIA, PUBLIC_CONTENT, PLUGIN_SANDBOX, and operator HOSTING_DEPLOYER, plus platform OAuth/session/provisioning variables and secret declarations. The retained SESSION, IMAGES, and ASSETS bindings are owned by that Astro deployment and contain no shared WiseWig tenant data. Never deploy the source superset dist/server/wrangler.json as a managed site; regenerate and pass the managed filename explicitly.

The initial Astro build compiles the self-hosted/managed-isolated source superset and may warn that its self-hosted secret prompts are unset. That warning is not the managed-isolated capability result. prepare --managed-isolated must remove those declarations; treat its doctor and the binding list from the generated Wrangler dry run as v2 release evidence.

These commands validate the local templates. Deploy the dispatch Worker before provisioning tenants. A managed deployment checks a trusted artifact, idempotently creates or reuses the exact tenant-named R2 bucket and Queue, verifies that the dispatch Worker is the Queue’s only consumer, uploads the user Worker to the production dispatch namespace, waits for the exact script identity, and only then marks the D1 tenant_workers record ready. Provider responses with a different resource, consumer, or script identity fail closed. See Tenant-specific Workers and Durable Objects.

For managed hosting, set SESSION_SECRET and OAUTH_CLIENT_SECRET interactively on wisewig-tenant-dispatch; never put them in Wrangler vars, shell history, customer Astro, or version control. Tenant user Workers receive neither. The private MCP Worker has its own SESSION_SECRET and OAUTH_CLIENT_SECRET; do not reuse those MCP credentials across Workers. Configure the OAuth issuer/endpoints/client ID and PLUGIN_TRUST_KEYS on the dispatcher, and ensure the provider permits each verified site’s exact /api/auth/callback URI. Plugin trust keys are public JWKs but remain environment-specific configuration. Self-hosted profiles continue to set their site SESSION_SECRET, OAUTH_CLIENT_SECRET, and PROVISIONING_SECRET independently.

Apply all infra/d1 migrations. The bearer-protected POST /api/admin/provision route then idempotently creates and verifies the tenant, site, domain, invited external identity, owner memberships, initial document, and theme. INSERT OR IGNORE never hides a conflicting hostname or identity mapping: the provisioner reads both back and fails with SITE_PROVISION_CONFLICT. The tenant object initializes matching draft/published snapshots so the first public request is valid and the invited owner can immediately authenticate.

For the first owner, visit /api/auth/start and complete the normal OIDC flow before provisioning. An authenticated identity that is not yet invited receives a 403 no-store receipt containing only its own stable issuer and subject; it receives no cookie, session, role, site data, or MCP authority. Put those exact coordinates in the bearer-protected provisioning request, then repeat login. Do not infer a subject from email and do not make uninvited identities automatic members.

Request flow

Ordinary custom-domain GETs check a named Cloudflare cache before Astro rendering. Cache hits avoid D1, authentication, and Durable Object work. Cache misses resolve only verified D1 domains and load the published document from the tenant object. Query-bearing requests, API routes, and requests carrying the exact server-issued wise_wig_mode=edit marker bypass this public cache. The marker is not authority: an invalid or missing signed session still falls back to published HTML without editor metadata.

Preview/edit requests validate the signed cookie against the required secret, resolve authoritative tenant/site membership through D1, and load draft state. Section, variant, structure, and publish endpoints enforce exact origin, optimistic document versions, guarded permissions, durable idempotency claims, and audit records before invoking tenant RPC.

Localhost deliberately retains the in-memory extension POC. The fixed local handoff endpoint returns 404 on a production/custom hostname and cannot mint a production signed session. Production extension entry discovers the non-secret editor endpoint from page metadata, then navigates through the same server-owned OAuth flow available at /api/auth/start; no OAuth client secret or session token enters extension code.

Deployment status

The five self-hosted framework Workers and the v2 managed-isolated tenant dispatch/user/operator templates build and dry-run with their declared bindings. Self-hosted private MCP resolves an OAuth-pinned tenant/site tuple from D1 and then selects that tenant’s Durable Object by the authenticated tenant ID; it has no Workers for Platforms dependency. The managed-isolated templates retain dispatch-based per-tenant Workers for v2. Real-workerd exercises the tenant and gateway boundaries locally; the beta release gate is now a deployed self-hosted full-profile workflow, not a Workers for Platforms drill.