External browser preview via mxcli tunnel-hub (single-app → multi-tenant)#11
Merged
Conversation
Verified end-to-end from a Claude Code web container: chisel's WebSocket reverse tunnel connects OUT through the session's mandatory MITM egress proxy to a Scaleway VPS relay on 443, and a real Mendix app running in the container renders in a browser via that relay. Closes the proposal's load-bearing open question (WebSocket-through-proxy) positive, and confirms the live-tunnel model (Option D) as preferred for Claude Code web — the app stays in the DEV container, the relay is static, nothing is pushed. Captures the implementation gotchas for the future tunnel-hub / dev --hub build: chisel default port is 8080 not 443 (-p 443 for --tls-domain); the client must be handed --proxy $HTTPS_PROXY explicitly; DNS + inbound 80 (ACME) are the only setup blockers; and ApplicationRootUrl must be set to the public subdomain (served over 443, not a raw port) for a correct SPA preview. Updates Open Q9 (resolved), the Option D coupling row, and the slice-3 risk note accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
… / mxcli tunnel-hub) Slice 3 of the warm-loop proposal: a locally-running Mendix app is made reachable in a browser at a public URL, without leaving the machine and without committing. The app stays in its (possibly egress-only) environment and reverse- tunnels out over 443 to a static hub; only live HTTP flows through the tunnel. - mxcli run --hub <url> [--hub-secret user:pass]: a cross-cutting ingress flag that implies --local. Boots the runtime with ApplicationRootUrl set to the hub URL (so the SPA/originURI work under that origin) and opens an embedded chisel reverse tunnel from the app port out to the hub. The proxy for the control connection is resolved from the environment honouring NO_PROXY, so an external hub goes through the egress proxy while a loopback hub connects directly. - mxcli tunnel-hub --domain <d> [--secret ...]: the static relay. Embeds a chisel reverse server with automatic Let's Encrypt (or --tls-cert/--tls-key) and proxies non-tunnel HTTP down the tunnel to the app, so everything rides a single 443 connection (works from egress-only environments like Claude Code on the web). Single-app for this slice; multi-tenant registration + subdomains are a follow-on. The chisel WebSocket transport was verified live out through the Claude Code web egress proxy to a real VPS relay; here the embedded client/server, the ApplicationRootUrl boot wiring, and a full run --hub -> hub -> app round trip are covered by an in-process tunnel test plus a local end-to-end boot. Mendix renders byte-identically through the hub's Host-rewriting backend, so no Host-preserving shim is needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Add the --hub/--hub-secret flags and an "External browser preview" section to the run-local skill and the docs-site page, and mark slice 3 shipped in the warm-loop proposal (code + in-process tunnel test + local end-to-end; external E2E against the Scaleway hub pending). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…oundation) The data plane for the multi-tenant hub: a Registry of registered previews keyed by identity (solution/project/branch/worktree) so reconnects keep a stable URL, with subdomain-slug allocation (project-branch; main collapses to the project; collisions disambiguated by worktree then a numeric suffix), reverse-port allocation from a range, heartbeat-based availability (available/stale) with TTL reaping, and deterministic sorting by last-used / registered / project+solution. Plus the registration API over it: POST /api/register (returns subdomain, url, reverse port, control URL, token, heartbeat interval), POST /api/status (heartbeat by bearer token), POST /api/deregister, GET /api/backends (for the admin page), with an optional shared-secret gate on registration. Foundation only — the 443 front (host routing + dynamic TLS + chisel control), the admin overview page, and the run --hub registration client follow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
… + admin page The serving plane for the multi-tenant hub, plus an optional hostname prefix. - server.go: one TLS front on 443 that routes by Host — the hub host serves the chisel control connection (WebSocket upgrade), the registration API, and the admin page; each <subdomain>.<domain> reverse-proxies to that preview's chisel reverse port with the public Host preserved. Certificates are issued per host via autocert, gated by a HostPolicy that only allows the hub host or a currently registered subdomain (so random subdomains can't drive issuance). A :80 listener serves ACME HTTP-01 + redirects to https. Friendly pages for unknown / offline previews. One embedded chisel reverse server fans in all client tunnels. - admin.go: a self-contained (no external assets) overview page that polls /api/backends and renders a sortable table — status (available/stale dot), solution, project, branch, clickable URL, registered / last-seen / last-used, uptime — sortable by any column, auto-refreshing. - Optional prefix: [prefix-]project[-branch] namespaces the hostname (organization / solution / team / env), passed through the registry + API and part of a backend's stable identity. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
… command Completes slice 4 end to end. `mxcli run --hub` now registers with the hub before booting: it auto-detects project (.mpr name) and branch (git), takes --hub-prefix/--hub-project/--hub-solution/--hub-branch/--hub-worktree overrides, POSTs /api/register, and uses the assigned subdomain URL as ApplicationRootUrl, tunnels to the assigned reverse port, and heartbeats (deregistering on exit). It falls back to the slice-1 single-app behaviour when the hub has no registration API, so both hub types keep working. `mxcli tunnel-hub` now runs the multi-tenant server (registry + registration API + admin overview + single-443 host-routing front with per-subdomain autocert), keyed off --domain (the wildcard base) with the control/admin at hub.<domain>. A background reaper frees ports for expired backends. An in-process integration test drives the whole path — register a preview, tunnel a backend to its reverse port with a real embedded chisel client, and assert a request to <sub>.<domain> routes through the tunnel with the public Host preserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…solution, overview) Update the run-local skill + docs-site with the multi-tenant flags (--hub-prefix/-project/-solution/-branch/-worktree), per-preview subdomains, the sortable availability overview, and stable URLs on re-register. Mark slice 4 built in the warm-loop proposal (external Scaleway E2E pending). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…t drop chisel's client MaxRetryCount defaulted to 0, so the reverse tunnel quit on the first disconnect it couldn't instantly recover (observed as "client: Give up" after a hub restart). Set MaxRetryCount = -1 so the preview reconnects indefinitely and survives a hub restart or a transient network blip. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The tunnel-hub work added a call path (docker.proxyForURL -> httpproxy.Config.ProxyFunc -> idna.Profile.ToASCII) into golang.org/x/net/idna, which govulncheck flags as GO-2026-5026 (failure to reject ASCII-only Punycode-encoded labels) in x/net@v0.54.0. Fixed in v0.55.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
The load-bearing project doc (CLAUDE.md Implemented list) and the README described only run --local; add the external browser preview (run --hub + multi-tenant tunnel-hub: subdomains, prefix/solution/branch/worktree, autocert, registration API, availability overview). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…rvice)
Replace the mxcli.org examples in CLI help, the run-local skill, and the docs-site
with example.com placeholders, and state plainly that there is no hosted hub —
you run your own mxcli tunnel-hub on a host you control. Add a security note (this
version uses a single shared secret with open registration, so keep the hub to
people you trust; per-tenant auth is a follow-up).
Also reconcile the preview docs with the shipped multi-tenant command: the hub is
started with --domain <base> (not the hub host) and previews are served at
<project>-<branch>.<base>, with the admin overview at hub.<base> — the old
single-app framing ("--domain hub...", "Preview available at hub...") was stale.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
…osure) Replace the remaining mxcli.org references (the proposal's verification record, code comment examples, and test fixtures) with example.com, so a scanner can't find and target the real hub host from the repo. Only hub-domain usage was changed; there was no www.mxcli.org (docs) or repo-link usage to preserve. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
ako
pushed a commit
that referenced
this pull request
Jul 23, 2026
Record the multi-project-solution orchestration as slice 5: several apps of one solution running side by side in a single container, each its own runtime + database with auto-allocated ports, all registered under one --hub-solution and grouped in the hub overview, with sibling URLs wired from the hub-assigned subdomains via a mxcli.solution.yaml manifest driven by `mxcli run --solution`. It's an orchestration layer over shipped primitives (per-project DB/deploy isolation, port-triple flags, the slice-4 hub's solution grouping) — no new runtime or hub mechanics. Also marks slices 3 & 4 verified-live and merged (PR #11). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
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.
What this adds
A locally-running Mendix app (
mxcli run --local) can now be made reachable in a browser at a public URL — without the app leaving its machine and without committing WIP. The app stays put and reverse-tunnels out over a single 443 connection to a static hub; only live HTTP flows through the tunnel. This works even from an egress-only environment (Claude Code on the web), which was the driving use case.Two commands:
mxcli run --hub <url> [--hub-secret user:pass]— a cross-cutting ingress flag (implies--local). It registers with the hub, boots the runtime withApplicationRootUrlset to the assigned URL (so the SPA/originURIwork under the public origin), opens an embedded chisel reverse tunnel, and heartbeats. The control connection's proxy is resolved from the environment honouringNO_PROXY, so an external hub goes through the egress proxy while a loopback hub connects directly.mxcli tunnel-hub --domain <d> [--secret ...]— the static relay, run once on a small VPS.Single-app (slices 1–3)
tunnel-hubfronts one app on the hub domain via chisel's--backend;run --hubsetsApplicationRootUrlto the hub URL. Validated live from a Claude Code web container against a Scaleway relay.Multi-tenant (slice 4)
tunnel-hub --domain mxcli.orgfronts many apps at per-preview subdomains over one 443:cmd/mxcli/tunnelhubpackage: aRegistryof previews keyed by prefix/project/solution/branch/worktree (re-registration keeps a stable URL), subdomain-slug allocation ([prefix-]project[-branch];main/mastercollapses to the project; collisions disambiguated by worktree then a numeric suffix), reverse-port allocation, heartbeat-based availability (available/stale) with TTL reaping, and deterministic sorting.POST /api/register,/api/status(heartbeat),/api/deregister,GET /api/backends, with an optional shared-secret gate.<subdomain>.<domain>reverse-proxies to that preview's tunnel with the public Host preserved. Certificates are issued per subdomain via autocert, gated to the hub host or a currently-registered subdomain. A:80listener handles the ACME challenge + https redirect.https://hub.<domain>/): a self-contained (no external assets) sortable table — status dot, solution, project, branch, clickable URL, registered / last-seen / last-used, uptime — sortable by any column, auto-refreshing.run --hubauto-detects project (.mprname) and branch (git), with--hub-prefix/--hub-project/--hub-solution/--hub-branch/--hub-worktreeoverrides; falls back to the single-app behaviour when the hub has no registration API.Also fixes the tunnel to retry forever (chisel's
MaxRetryCountdefaulted to 0, so it gave up on the first drop) so a preview survives a hub restart or a network blip.Testing
go build ./...,go vet,gofmtclean.cmd/mxcli/tunnelhub(registry allocation/identity/availability/sorting, the registration API, front host-routing) and an in-process end-to-end integration test: register a preview, tunnel a backend to its reverse port with a real embedded chisel client, and assert a request to<sub>.<domain>routes through the tunnel with the public Host preserved.cmd/mxcli/docker:NO_PROXY-aware proxy resolution, an in-process tunnel round-trip, andApplicationRootUrlboot-config injection.mxcli.orgwildcard DNS): both the single-app and multi-tenant paths verified from a Claude Code web container — chisel's WebSocket survives the session's MITM egress proxy, the Mendix app renders over HTTPS on its assigned subdomain, and it appears available in the overview. Also verified from a second, independent Claude Code web session running a from-source build.Follow-ups (out of scope)
Design record and the verification write-up are in
docs/11-proposals/PROPOSAL_mxcli_dev_warm_loop.md.🤖 Generated with Claude Code
https://claude.ai/code/session_01JXnEgoc2NQP1Y2TWMCMXC4
Generated by Claude Code