Skip to content

[miniflare] Support remote Hyperdrive bindings in local dev - #14900

Open
mack-erel wants to merge 5 commits into
cloudflare:mainfrom
mack-erel:hyperdrive-remote
Open

[miniflare] Support remote Hyperdrive bindings in local dev#14900
mack-erel wants to merge 5 commits into
cloudflare:mainfrom
mack-erel:hyperdrive-remote

Conversation

@mack-erel

@mack-erel mack-erel commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Fixes #14842.

What

hyperdrive bindings are local-only today: wrangler dev connects straight to localConnectionString, so a database that is only reachable through the deployed Hyperdrive configuration — for example one whose origin is a private-network VPC Service — cannot be exercised locally at all.

This adds remote: true for hyperdrive bindings, matching the existing opt-in on kv_namespace / r2_bucket / d1:

{
	"hyperdrive": [{ "binding": "HYPERDRIVE", "id": "<your-hyperdrive-id>", "remote": true }]
}

With it set, local dev talks to the deployed Hyperdrive configuration and standard drivers (mysql2, pg) work unchanged.

How

A local TCP bridge, not a Worker designator. Miniflare's HyperdriveProxyController gains createRemoteTcpBridge, which listens on 127.0.0.1 and relays each connection byte-for-byte to the edge through the remote-bindings proxy's existing connect handler (#14712). The hyperdrive:<name> designator stays an external.tcp service pointing at that bridge, so workerd is unmodified.

That last part is deliberate rather than incidental: pointing a Hyperdrive designator at a Worker service segfaults workerd (cloudflare/workerd#6901). I built workerd from source to check whether that was a quick fix — even with the client-side lifetime fix applied (attaching the adapter and WorkerInterface to the returned stream, mirroring connectImpl), the crash goes away but bytes still don't flow through a Worker target due to a second, target-side teardown issue. So the relay runs in the Miniflare Node process instead.

Credential seeding. The edge mints per-session credentials, so a header-guarded MF-HD-Seed endpoint on ProxyServerWorker returns the edge binding's connectionString, and seedRemoteHyperdriveBindings writes it into the local binding config before the (synchronous) buildMiniflareBindingOptions runs. That is what lets drivers authenticate through the edge proxy, and why localConnectionString becomes optional for these bindings.

Fully opt-in — without remote: true, the existing local-only path is unchanged.

Testing

Four new tests in remote-bindings-connect.spec.ts drive a raw TCP client through createRemoteTcpBridge into a real (bundled) ProxyServerWorker, asserting bidirectional byte relay, plus the MF-HD-Seed happy path, a 400 on a missing MF-Binding header, and a 404 on an unknown binding. Suite 19/19; packages/wrangler hyperdrive tests 65/65; turbo build check:type 34/34 across miniflare + wrangler + remote-bindings; oxfmt and oxlint clean.

Also verified end-to-end against a real Hyperdrive configuration (MySQL behind a Cloudflare Tunnel): a stock mysql2 pool authenticates and SELECT VERSION() returns the origin server's version rather than the proxy banner — i.e. queries reach the real database, as in production.

Known constraints

  • Seeding runs once per dev session, since the edge credentials are per-session.
  • The bridge inherits the raw-TCP relay's limits from [miniflare] Support connect() on remote VPC Network bindings in local dev #14712: no TCP half-close propagation, and no backpressure plumbing.
  • MF-Connect-Address is sent as a fixed hyperdrive.local:0 — Hyperdrive's edge connect() ignores the address, but the relay path requires the header to be present.

On the opt-in semantics

#14842 raised three possible shapes for the opt-in, and this PR implements the simplest one (remote: true, consistent with the other bindings). The other two are implemented on the same core and I am happy to swap either in: a fallback variant (fall back to localConnectionString when the remote session cannot be established), and an experimental-flag-gated variant. I opened this one as a concrete starting point rather than leaving the question abstract — say the word and I will re-submit in whichever direction you prefer.


  • Tests
    • Tests included/updated
  • Public documentation
    • Documentation not necessary because: remote carries the same meaning here as the already-documented opt-in on KV, R2 and D1 bindings. Happy to open a Cloudflare Docs PR for the Hyperdrive binding page if you'd like it called out there.

A raccoon waving hello

…idge

Hyperdrive bindings could only ever point at a local database. Accept an
object entry carrying a `remoteProxyConnectionString` alongside the existing
plain connection string, which opts the binding into remote mode.

For a remote binding, `HyperdriveProxyController.createRemoteTcpBridge`
listens on 127.0.0.1 and relays each inbound connection byte-for-byte to the
edge Hyperdrive binding over a WebSocket to the remote proxy's `connect`
handler. The `hyperdrive:<name>` `external.tcp` designator points at that
bridge, which keeps workerd unmodified — aiming a Hyperdrive designator at a
Worker service crashes it.

Local-only bindings keep taking the existing code path unchanged.

Co-Authored-By: Claude Opus 5 <[email protected]>
@changeset-bot

changeset-bot Bot commented Jul 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 429ba95

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 8 packages
Name Type
miniflare Minor
wrangler Minor
@cloudflare/deploy-helpers Patch
@cloudflare/pages-shared Patch
@cloudflare/remote-bindings Patch
@cloudflare/runtime-types Patch
@cloudflare/vite-plugin Patch
@cloudflare/vitest-pool-workers Patch

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

@github-project-automation github-project-automation Bot moved this to Untriaged in workers-sdk Jul 29, 2026
@workers-devprod
workers-devprod requested review from a team and edmundhung and removed request for a team July 29, 2026 00:20
@workers-devprod

workers-devprod commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/hyperdrive-remote-bindings-local-dev.md: [@cloudflare/wrangler]
  • packages/config/src/bindings.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/hyperdrive/hyperdrive-proxy.ts: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/hyperdrive/index.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/shared/remote-bindings-connect.spec.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/src/index.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/src/seed-hyperdrive-bindings.ts: [@cloudflare/wrangler]
  • packages/remote-bindings/templates/remoteBindings/ProxyServerWorker.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/config/binding-local-support.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/config/environment.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/config/validation.ts: [@cloudflare/wrangler]
  • packages/workers-utils/src/worker.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/tests/dev/miniflare-hyperdrive.test.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/api/startDevWorker/LocalRuntimeController.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/dev.ts: [@cloudflare/wrangler]
  • packages/wrangler/src/dev/miniflare/index.ts: [@cloudflare/wrangler]

@pkg-pr-new

pkg-pr-new Bot commented Jul 29, 2026

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@14900

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@14900

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@14900

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@14900

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@14900

miniflare

npm i https://pkg.pr.new/miniflare@14900

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@14900

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@14900

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@14900

@cloudflare/vitest-pool-workers

npm i https://pkg.pr.new/@cloudflare/vitest-pool-workers@14900

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@14900

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@14900

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@14900

wrangler

npm i https://pkg.pr.new/wrangler@14900

commit: 429ba95

devin-ai-integration[bot]

This comment was marked as resolved.

mack-erel and others added 4 commits July 29, 2026 09:38
Allow `remote: true` on a `hyperdrive` binding to opt local dev into
connecting to the deployed Hyperdrive configuration at the edge instead of a
local database. Hyperdrive moves from `local-only` to `local-and-remote` in
the binding support table, and the config validator accepts and validates the
new field.

When the flag is set and a remote proxy session is available,
`hyperdriveEntry` hands miniflare the object form carrying the
`remoteProxyConnectionString` so it can stand up the TCP bridge.
`localConnectionString` is no longer required for these bindings, since there
is no local database to point at.

A session is not always there to be had — logged out, offline, or remote
bindings turned off — so without one the binding falls back to its
`localConnectionString` with a warning, and errors with instructions when it
has none. Otherwise the empty connection string reaches miniflare and fails
its URL validation with an opaque error.

Co-Authored-By: Claude Opus 5 <[email protected]>
…entials

The edge Hyperdrive proxy mints per-session credentials and uses the config id
as the database name, so a database client reaching it through the TCP bridge
must present those values — the user's local placeholder credentials only get
as far as the server greeting.

Add an `MF-HD-Seed` endpoint to ProxyServerWorker that returns a binding's
edge `connectionString`. It is guarded on the header so it never intercepts
ordinary connect/RPC/fetch traffic, and returns 400 without `MF-Binding` and
404 for a binding that exposes no connection string.

`seedRemoteHyperdriveBindings` fetches that value for every remote Hyperdrive
binding and writes it into the local binding config.
`LocalRuntimeController` awaits it once the remote proxy session is ready and
before miniflare options are built, since that step is synchronous. It is a
no-op without a proxy session or remote Hyperdrive bindings.

The seeded value is a live credential, so it is never logged.

Co-Authored-By: Claude Opus 5 <[email protected]>
Extend the remote bindings connect suite with the Hyperdrive path: a test
that drives a raw TCP client through `createRemoteTcpBridge` into a real
ProxyServerWorker and asserts bytes are relayed in both directions, and three
covering the `MF-HD-Seed` endpoint — the happy path returning the binding's
connection string, a 400 for a missing `MF-Binding` header, and a 404 for an
unknown binding.

Co-Authored-By: Claude Opus 5 <[email protected]>
@mack-erel
mack-erel force-pushed the hyperdrive-remote branch from 147ec12 to 429ba95 Compare July 29, 2026 00:45
@mack-erel

Copy link
Copy Markdown
Contributor Author

Thanks — both are fair. Fixed the first; here's my reasoning on the second.

Empty connection string / cryptic error (dev.ts:491) — fixed. hyperdriveEntry now throws a UserError when a binding has remote: true but neither a remote proxy session nor a localConnectionString, instead of passing "" down to Miniflare's URL validation. The message points at both remedies (authenticate for remote mode, or set localConnectionString / the env var).

I also covered the adjacent case Devin didn't flag: when the session is unavailable but a localConnectionString exists, the binding still falls back to it — that's the right behaviour for offline dev, so I kept it, but it now emits a logger.once.warn so a silent remote→local downgrade doesn't turn into a "why is my data different" debugging session. Four unit tests in packages/wrangler/src/__tests__/dev/miniflare-hyperdrive.test.ts cover local / remote-with-session / remote-with-neither / remote-with-fallback.

MF-HD-Seed guarded only by a header — worth stating explicitly, but I think it's the correct boundary rather than a gap. The endpoint sits behind exactly the same gate as the existing fetch and JSRPC relays on this worker: reaching it requires the remote proxy session URL, and the binding name is validated the same way. More importantly it grants no capability beyond what the session already confers — anyone who can reach the session can already open a tunnelled connection to the same database through the connect() relay, so handing back the connection string doesn't widen the blast radius. The seeded value is never logged on either side.

That said, if you'd prefer a stronger guard — a per-session nonce, or folding the seed into the JSRPC surface rather than a plain header — I'm happy to add it.

Also fixed two unrelated CI failures: the changeset listed @cloudflare/remote-bindings, which is a private package, and the PR description was missing the template checklist.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

🚀 Feature: support remote Hyperdrive bindings in local dev (raw TCP relay)

2 participants