opensci.devopensci.dev

JupyterHub + MyST: A Proxy Problem

Live preview is really effective

One of the really delightful parts of using the Curvenote CLI[1] or modern MyST Markdown stack is how good the local authoring experience has become.

Using the CLI, you can run a single command and immediately get a live, interactive preview of your article or book. As you edit Markdown, notebooks, LaTeX!, or configuration files, the rendered output updates immediately. Showing you your content in a form that is very close to what will eventually be published to the web. This brings the developer-style hot reload experience into Markdown based scientific and technical writing.

This matters - writing is iterative and exploratory, and fast feedback loops can help shape how people write, structure, and reason about their work.

Locally, this experience works extremely well powered by a start command, that fetches, and starts a dynamic theme server to render the content.

Some communities are working on shared infrastructure like JupyterHub and would like to also do their writing there, alongside the computational work. This is where the experience starts to break down.

This post is about why.

A quick recap: what live preview actually is

When you run curvenote start or myst start locally, you are not just building HTML and opening a static HTML file. You are launching a small web stack.

At a high level, there are three moving parts:

  • The CLI, which watches your files and builds structured content

  • A content server, which exposes the processed MyST document data (AST)

  • The theme server, a (currently) Remix-based web application responsible for rendering that content as a modern website

The browser talks to the theme server. The theme server talks to the content server. And live updates are pushed via WebSockets so that changes re-render without a full page reload.

The overall architecture is intentional but was not explicitly designed for local previews. The stack itself was primarily aimed publishing effectively to the web, enabling things like server-side rendering for fast page loads, distributed content, incremental/partial rebuilds[2] and networked knowledge via external cross referencing between published MyST websites.

On a local machine, we reuse the theme servers that are intended for dynamic hosting, to provide the preview experience. While the local architecture still makes sense for live preview stack the theme servers like book-theme are not optimized for it.

Theme servers are optimized for:

  • Server-side rendering (fast first load)

  • Client-side navigation (smooth transitions)

  • Code-splitting and dynamic/lazy asset loading

  • Efficient backend communication with http request management

All of these are strengths when publishing to the web, they are also, as it turns out, exactly what makes things difficult on JupyterHub.

Enter JupyterHub (and the proxy)

In a typical JupyterHub deployment:

  • Each user runs on their own single-user server

  • That server is not directly exposed to the internet

  • All external access is routed through a proxy (Jupyter Server Proxy)

  • Additional services must be explicitly exposed, usually by port

From a security and multi-user perspective, this makes sense but it means that any web service you want to access from the browser—other than Jupyter itself—has to pass through that proxy layer.

Some effort is being put into dealing with this by not using the live preview that myst start provides, instead falling back to myst build and generating static html on change, like the jupyter-myst-build-proxy project. While this can work, the html based build will be slower and less interactive than the live preview.

So it’s worth trying to get the myst start experience working on a hub.

Why this is not “just expose a port”

At first glance, the problem sounds simple:

“Can’t we just proxy the ports that the live preview needs?”

This is where I think most people would start, i.e. let’s plan on exposing the theme server and content server ports, and do some websocket forwarding to keep things live.

This is reasonable and as the proxy works by mapping server ports to URLs like: /user/steve/proxy/3800 on the Hub, this seems to come down to path rewriting. The MyST and Curvenote CLIs both accept a BASE_PATH environment variable, which gets us some way, but from experience there are multiple layers of problems to solve - which are discovered only one you descend into the rabbit hole.

I have spent time trying to get the theme servers working “as-is” across the proxy and hit various hurdles on the way, here are most of those:

  • We need to rewrite the paths the BATH_PATH already takes care of, these are things like relative urls in the navigation, the site configuration and MyST AST data structures which also happens in the content server

  • The theme server build need to be modified to rewrite the paths to the Javascript and CSS assets needed for initial page load, requiring hooking into the theme server’s build process itself

  • After initial page load or page refresh (that used SSR), when a user navigates the client side code takes over and exposes other issues:

    • Because of the javascript chunking that Remix does, we have javascript files that need to be loaded as the URL/route changes. These contain relative import statements which also need to be parsed and rewritten.

    • Lazy route discovery means the theme server dynamically loads a manifest on navigation, with paths to JS files, which again need to be rewritten[3].

These are the challenges of moving the current theme servers behind a proxy. My take is that we should just build out a different theme server that is both designed for local previews and solves for the proxy scenario in the first place.

As myst-theme is not just a theme server but a collection of reusable packages intended to support the development of diverse themes in the community, this is not as bad as it sounds.

Where this leaves us

To get a first-class preview experience on JupyterHub, we cannot simply “port” the local setup behind a proxy.

We need to rethink the preview stack itself and at Curvenote we have already started down that path with one of our customers b.next who have really embraced Hub based publishing using Curvenote within their Digital Lab environment.

We have an alternative approach we’ve been working on, and it’s in testing now. It is one that preserves fast feedback and live updates, but is designed from the ground up to be proxy friendly and to prioritize the writing, preview and review workflow.

That work will be open source, and it is very close to being ready to share.

More soon.

License

Copyright © 2026 Purves.

Footnotes
  1. The Curvenote CLI is builds on the mystmd command line tool, adding functionality to interact with and publish to the Curvenote SCMS

  2. This ain’t sphinx

  3. We want to enable all Remix’s future flags as Remix is/has been replaced with React Router v7 and the theme servers will have to migrate to that framework soon to continue support, these future flags represent standard features in React Router v7k.

Footnotes
  1. The Curvenote CLI is builds on the mystmd command line tool, adding functionality to interact with and publish to the Curvenote SCMS

  2. This ain’t sphinx

  3. We want to enable all Remix’s future flags as Remix is/has been replaced with React Router v7 and the theme servers will have to migrate to that framework soon to continue support, these future flags represent standard features in React Router v7k.