fix(cloudflare): make sandboxed plugins a paid-plan opt-in - #2351
Conversation
🦋 Changeset detectedLatest commit: 7418fcd The changes in this PR will be included in the next version bump. This PR includes changesets to release 17 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
emdash-demo-cache | 6f46425 | Aug 21 2026, 12:35 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | 92e74ae | Aug 16 2026, 10:42 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
emdash-demo-do | 6f46425 | Aug 21 2026, 12:35 PM |
There was a problem hiding this comment.
Approach judgment: This is the right change for the stated problem. Gating dynamic plugins behind real sandbox availability keeps the default Cloudflare scaffold deployable on the Workers free tier, gives users a clear opt-in path, and surfaces missing setup in the admin UI instead of a late 503. The maintainer-requested origin makes the lack of a separate Discussion reasonable. The design is additive and backwards-compatible: new scaffolds default to worker_loaders commented out; existing sites with the binding keep reporting sandboxAvailable: true and see no behavior change.
What I checked: full diff, the changed runtime/admin/create-emdash files, route structure around marketplace/registry, the manifest endpoint route, SandboxRunner usage, the existing globalThis singleton patterns in emdash-runtime.ts, and the changeset config (which groups emdash, @emdash-cms/admin, and create-emdash in one fixed release, so the two changesets cover the admin changes as well).
Headline conclusion: Implementation is solid and well-tested. I found one AGENTS.md convention issue with a new module-scope singleton cache and one UX suggestion to avoid rendering the marketplace UI while the manifest is still loading. Both are fixable in small follow-up edits; neither blocks understanding the PR.
Scope checkThis PR changes 693 lines across 20 files. Large PRs are harder to review and more likely to be closed without review. If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs. See CONTRIBUTING.md for contribution guidelines. |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/registry-moderation
@emdash-cms/registry-verification
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
docs | 8c31ec2 | Sep 11 2026, 09:59 AM |
There was a problem hiding this comment.
This is the right change for the stated free-tier deploy blocker: gating dynamic plugins behind real sandbox availability keeps the default Cloudflare scaffold deployable, gives users a clear opt-in path, and surfaces missing setup in the admin UI instead of a late 503. The design is additive and backwards-compatible throughout.
In this re-review I checked the full diff, the changed runtime/admin/create-emdash files, the manifest endpoint, SandboxRunner usage, the existing globalThis singleton patterns, and the changeset config.
Headline conclusion: the implementation is solid and well-tested. The blocking-style concern from the previous review — the new module-scope availability cache not living on globalThis — has been fixed: getSandboxRunnerAvailabilityCache() now stores the WeakMap behind Symbol.for("emdash:sandbox-runner-availability") on globalThis, matching the DB_HOLDER_KEY / SEED_HOLDER_KEY patterns already in emdash-runtime.ts.
The previous UX suggestion about avoiding the marketplace UI while the manifest is still loading has been addressed with an explicit design choice: the routes wait for the manifest before deciding, and deliberately fall through to the browse/detail UI during load so the common paid-tier path doesn't flash the unavailable prompt. I'm not raising it again as a finding because that's a reasonable judgment call.
I found no new logic bugs, regressions, security issues, or AGENTS.md convention violations. Test coverage is good across create-emdash, emdash runtime, and admin routes. Changeset grouping covers @emdash-cms/admin via the fixed release. LGTM.
…gins-worker-loader
There was a problem hiding this comment.
This is the right change for the stated free-tier deploy blocker. Gating the Cloudflare sandbox runner on the real worker_loaders binding makes new Cloudflare scaffolds deployable on the Workers free plan, preserves paid-tier behavior when the binding is present, and routes install/update requests through the existing SANDBOX_NOT_AVAILABLE envelope instead of a late runtime failure. It is additive and backwards-compatible for existing Cloudflare and Node projects.
I reviewed the full diff, the changed runtime / admin / create-emdash files, the Cloudflare adapter, the SANDBOX_NOT_AVAILABLE paths in the marketplace and registry handlers, the sandbox runner virtual-module wiring, the four Cloudflare templates, and the changeset. I also checked the updated docs against the implementation (the writing-emdash-docs skill was not available via the tool, so I verified the prose manually).
Headline conclusion: the implementation is solid and well-tested. No new logic bugs, regressions, security issues, or AGENTS.md convention violations were introduced in this revision. I am not re-raising the two prior-review concerns that were already addressed (the globalThis availability-cache pattern and the deliberate marketplace-loading UX choice), because the current diff shows them resolved or unchanged.
I have two very minor test-maintenance suggestions, both non-blocking.
Findings
-
[suggestion]
packages/create-emdash/tests/flags.test.ts:373-381The
HELP_TEXTsync-check list was not updated for the new flags. The test still passes because the old flags are present, but it no longer actually checks that every supported flag is documented.for (const flag of [ "--template", "--platform", "--pm", "--package-manager", "--install", "--no-install", "--sandboxed-plugins", "--no-sandboxed-plugins", "--yes", "--force", "--help", ]) { -
[suggestion]
packages/core/tests/unit/astro/integration/marketplace-sandbox.test.ts:6The inner test description says "themes" but the test configures the plugin marketplace. This appears to be a copy/paste leftover.
it("allows the marketplace to remain available when sandboxed plugins are disabled", () => {
There was a problem hiding this comment.
This is the right change for the free-tier deploy blocker. Gating the Cloudflare sandbox runner on the actual worker_loaders binding keeps new Cloudflare scaffolds deployable on the Workers free plan, preserves paid-tier behavior when the binding is present, and routes install/update requests through the existing SANDBOX_NOT_AVAILABLE envelope instead of a late runtime failure. It is additive and backwards-compatible for existing Cloudflare and Node projects.
Re-review status: Both prior emdashbot[bot] suggestions were addressed in this revision — the HELP_TEXT sync-check list now covers --sandboxed-plugins / --no-sandboxed-plugins, and the marketplace-without-runner test description no longer says "themes".
I reviewed the full diff, the changed runtime / create-emdash files, the Cloudflare adapter, the SANDBOX_NOT_AVAILABLE paths in packages/core/src/api/handlers/marketplace.ts and registry.ts, the sandbox() implementation and tests, the four Cloudflare templates, and the updated docs. I verified that the templates export PluginBridge, that the handlers return SANDBOX_NOT_AVAILABLE when the runner is missing or unavailable, and that sandbox() honors CLOUDFLARE_ENV and the exact LOADER binding name.
I found one remaining issue: the docs incorrectly state that Wrangler named environments do not inherit top-level bindings. That will lead users to repeat the LOADER binding in every environment unnecessarily. The fix is a wording adjustment; the implementation itself is solid.
Everything else is clean: no new logic bugs, security issues, AGENTS.md convention violations, or logged-out hot-path query regressions. The changeset is adequate and the tests cover the new behavior.
|
I checked the named-environment suggestion against Wrangler 4.124.0 rather than changing the guide: The existing wording is therefore accurate, so I have left it unchanged. ~ 🤖 Codex |
…gins-worker-loader # Conflicts: # docs/src/content/docs/plugins/installing.mdx # docs/src/content/docs/reference/configuration.mdx
…gins-worker-loader # Conflicts: # docs/src/content/docs/deployment/plugin-sandbox.mdx
What does this PR do?
New Cloudflare scaffolds leave the paid-plan Worker Loader binding disabled by default, so a generated site can deploy on the Workers free plan without editing
wrangler.jsonc.create-emdashasks whether to enable sandboxed plugins on Cloudflare, defaults to no, and supports--sandboxed-plugins/--no-sandboxed-pluginsfor non-interactive use. It normalizes both the current commented binding and the older multiline binding.sandbox()from@emdash-cms/cloudflarereads the project’s Wrangler config at build time and selects the Cloudflare runner only when theLOADERbinding is present. It honors the named environment selected throughCLOUDFLARE_ENV, so Worker Loader remains the source of truth for default and named environments.SANDBOX_NOT_AVAILABLEuntil a runner is enabled.Existing Cloudflare projects keep their checked-in Wrangler configuration. Existing Node sites with the workerd runner keep the same runtime behavior. The implementation originally authored by @MattieTK remains in its original commit; current
mainwas merged without rebasing or force-pushing.Part of #1680.
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges except in translation PRs — a workflow extracts catalogs on merge tomain. (n/a: no admin UI changes)AI-generated code disclosure
Screenshots / test output
Screenshots are not applicable because this PR does not change the UI.
Verified on the current head:
The
blog-cloudflarescaffold also completedastro build, logging the expected build-time warning that sandboxed plugins are disabled. Its redirected Wrangler 4.124.0 dry run completed with KV, D1, R2, Images, and Assets bindings and no Worker Loader binding.