Data Science as a Reproducible Link

livelink turns an R project, or an R or Python Shiny app, into a reproducible link, a single URL that runs in the browser with no compute server behind it and reads back into the original files

r
r-package
webr
shinylive
quarto
webassembly
CRAN
Author

James Balamuta

Published

July 16, 2026

Abstract

Data science can travel as a reproducible link. Unlike an ordinary URL, the link does not point at your code. It contains it. livelink compresses your files, or a whole R or Python Shiny app, into the part of a URL that browsers never send to a server, so opening it runs the analysis in the browser, on runtimes webR and Shinylive already host, with nothing to install and no compute server to maintain. This post traces how R code becomes one of these links, why the round trip loses nothing, and how a punchy LinkedIn one-liner became something you can actually hand to someone, with the livelink package built on webR’s own sharelink feature.

livelink hex logo: a stack of code lines receding into the distance with an outbound arrow

livelink hex logo: a stack of code lines receding into the distance with an outbound arrow

The first time someone sends you a livelink URL and you click it, the browser does something that quietly rearranges your mental model. A tab opens, R or Python boots up inside it, your correspondent’s code is already sitting in the editor, and the plot is already drawn. You check the network tab out of habit, and there is nothing to check. No request went out to fetch the code, because there was no code to fetch. It arrived in the link, tucked into the fragment after the #, the one part of a URL a browser never sends to anyone.

Sit with that for a second, because what it means is easy to miss. No server ran the code. The whole analysis, source and data and all, rode in on the URL you clicked and computed in your own tab. None of that would be possible without WebAssembly, a portable compiled format that browsers now run at close to native speed. webR is the WebAssembly build of R that runs in a tab, Pyodide is the same for Python, and Shinylive stitches either one into a live Shiny app. webR and Shinylive stood those runtimes up at their own expense, and livelink only aims a link at them.

Once you accept it, the tidiest description of what that link does is almost a slogan. Data science is a reproducible link, not a link to a dashboard someone else keeps running for you, not a link to a repository you have to clone and install first, but the running analysis itself, handed over as a single string of text you can open and run.

I did not arrive at that idea gently. Its shorter, punchier form, “data science is a link,” first struck a nerve in a LinkedIn post in the summer of 2025, which drew 676 reactions and became the most popular thing I have ever written there. The size of the response told me the phrase was carrying more than a party trick, and livelink is my attempt to make it literally true, in both directions. It packs your work into a live link, which is where the name comes from, and it reads any link back into files. It is a shared experiment more than a product, and even the sharelink URLs it writes are ones webR already knew how to open.

livelink did not invent any of this. The webR REPL and the Shinylive editor each had a share link before livelink existed, and livelink only builds on both. It is not the only tool chasing the idea now, either. The notebook world has been converging on it quickly, with QuantStack’s notebook.link arriving for Jupyter in January 2026 and JupyterLite 0.8 adding notebook sharing in July 2026.

Regular readers have met the other half of this idea. My peeky package makes the point that a Shinylive app keeps no secrets, since to run in the browser it hands its entire source to every visitor. livelink is that same property pointed the other way and used on purpose. If a browser app is going to give away everything it needs to run, you may as well put everything it needs to run in the link and call it a feature. All of which changes the answer to a small, familiar question. “Can you share that R code?” used to mean twenty minutes of installation drama. Now it is just a link, and “it works on my machine” has become a harder excuse than it used to be.

Two ways to share R code

In the usual arrangement, sharing R means shipping instructions for reproducing your environment. Install R, then the packages, then run this. The code travels as text, but the ability to run it travels as a checklist you hope the other person completes. The runtime lives on their machine, and you have no idea whether it is there. This is why a reproducible example turns into a support thread and why a workshop opens with setup instead of the material.

The other way moves the runtime instead of the code. Those WebAssembly runtimes run the interpreter inside the recipient’s browser tab, downloaded on demand the way a page downloads a font, so nothing is installed because nothing needs to be. The R that runs your code arrives with the page, and the only thing you have to deliver is the code. Both tools already ship a share button that packs the current editor into a link, and that button is where I first met the idea, in Shinylive’s editor and soon after in webR’s.

Two lanes from the same project, shown as a file tree under analysis with report.R, data.csv, and utils.R. The top lane, labelled send the instructions, runs the project through a dashed, uncertain path into a numbered stepper (install R, install the packages, source the script) carrying a twenty-minute time badge, then into a laptop with a centered warning triangle and the note runs, if it installs, and only if it compiles. The bottom lane, labelled send a link, runs the same project through a solid path into one webr.r-wasm.org URL with a #code fragment and a one-click cursor, then into a browser already running a plot.

Two ways to share R. One sends setup instructions and leaves the runtime on the recipient’s machine, uncertain and slow. The other sends a link, and the runtime arrives with the page.

Two lanes from the same project, shown as a file tree under analysis with report.R, data.csv, and utils.R. The top lane, labelled send the instructions, runs the project through a dashed, uncertain path into a numbered stepper (install R, install the packages, source the script) carrying a twenty-minute time badge, then into a laptop with a centered warning triangle and the note runs, if it installs, and only if it compiles. The bottom lane, labelled send a link, runs the same project through a solid path into one webr.r-wasm.org URL with a #code fragment and a one-click cursor, then into a browser already running a plot.

Two ways to share R. One sends setup instructions and leaves the runtime on the recipient’s machine, uncertain and slow. The other sends a link, and the runtime arrives with the page.

Building on the share button

livelink takes that idea and turns it from a button into a toolkit. You know how you zip a folder to share it? This is that, except the archive is the URL itself, and clicking it does not just unpack the project, it runs it. An autorun setting controls how much runs when it opens, all the files, none of them, or just the ones you name. Where the built-in buttons pack whatever sits in one editor, livelink packs arbitrary R: a single script, a named set of files, a paired exercise and worked solution, or a whole directory of course scripts, each written as ordinary R in braces rather than a string of escaped newlines. It works in both directions, so it does not only write links, it reads any link back into files, including the msgpack links webR’s own button produces. And it runs where you already work, from the R console and inside a knitr or Quarto document, so a chunk can carry its own runnable link.

Two editor windows side by side. On the left, the webR REPL at webr.r-wasm.org shows a script.R tab with a new-tab button, code that plots mtcars, an R console running it, and a plot with axes. On the right, the Shinylive editor at shinylive.io shows an app.R tab, Shiny app code, and a running app with a slider and a histogram. Each toolbar has run, copy, and a highlighted share control, and the share control has opened a popup showing that window's #code= link with a Copy button. The two links differ, one ending in the gzip and base64 flags and the other an LZ-string. livelink writes both kinds from R.

The share button in the webR REPL and in the Shinylive editor opens a popup with that window’s #code= link and a copy button. The two links differ, each in its own encoding, and livelink writes both from R.

Two editor windows side by side. On the left, the webR REPL at webr.r-wasm.org shows a script.R tab with a new-tab button, code that plots mtcars, an R console running it, and a plot with axes. On the right, the Shinylive editor at shinylive.io shows an app.R tab, Shiny app code, and a running app with a slider and a histogram. Each toolbar has run, copy, and a highlighted share control, and the share control has opened a popup showing that window's #code= link with a Copy button. The two links differ, one ending in the gzip and base64 flags and the other an LZ-string. livelink writes both kinds from R.

The share button in the webR REPL and in the Shinylive editor opens a popup with that window’s #code= link and a copy button. The two links differ, each in its own encoding, and livelink writes both from R.

None of this stands on its own. By default every link points at an environment someone else keeps running, webr.r-wasm.org for R and shinylive.io for Shiny, and it is written in webR’s own sharelink format, so it opens there exactly like one from the share button. That default is not a lock-in, though. Point the base URL at a webR or Shinylive instance you deploy yourself, on your own domain or an internal network, and the same links open against your copy instead. livelink hosts nothing and runs nothing of its own. With the hard part already built, the only open question is how far “just send a link” can go.

Everything after the hash

The trick, such as it is, lives in one character. Take any URL apart and everything after the # is the fragment. It was meant to point at an anchor inside a page, and browsers treat it as strictly client-side, so the fragment is never included in the HTTP request to the server. Single-page apps lean on this for routing; livelink leans on it for storage. Whatever livelink writes after #code= stays on the visitor’s machine, and the R runtime in the tab reads it from there.

A webR URL broken into labelled segments: the base address webr.r-wasm.org, the version latest, a mode query selecting which panels appear, the #code= fragment holding a base64 payload of the files, and trailing flags describing the encoding.

A webR URL in five parts: the base address, the version, the panels to show, the #code= fragment carrying your files, and the flags saying how the payload was encoded.

A webR URL broken into labelled segments: the base address webr.r-wasm.org, the version latest, a mode query selecting which panels appear, the #code= fragment holding a base64 payload of the files, and trailing flags describing the encoding.

A webR URL in five parts: the base address, the version, the panels to show, the #code= fragment carrying your files, and the flags saying how the payload was encoded.

That is why there is no backend to talk to, though it is worth being exact about what “no server” means. Something still serves the page, but only a static web host, the same kind behind any ordinary website. Here webr.r-wasm.org hands out the webR runtime, and a GitHub Pages or Netlify site would do just as well if you publish the files yourself. It gives every visitor the identical bytes and never sees your code, which lives in the fragment. What is gone is the compute server. No one has to keep R and its packages installed on a box, or stand up and pay for and patch a web IDE, because the runtime boots inside the reader’s own tab and the analysis rides in on the URL. A static file host is the whole operational footprint, which makes livelink an ephemeral IDE for data science: editor, runtime, and code assembled in the tab when the link opens, and gone when it closes.

A ?mode='editor-plot' query in front of the fragment tells webR which panels to show, so you can read a link and know the recipient will land on an editor and a plot pane and no terminal.

The pipeline that gets code into the fragment

Turning files into a fragment is a short, deterministic pipeline. livelink takes the files you hand it, a single script or a named list of them, and records each one as a small JSON object of its name, its path in the virtual filesystem, and its text:

[{ "name": "script.R", "path": "/home/web_user/script.R", "text": "plot(1:10)" }]

It compresses that JSON with gzip (memCompress()), base64-encodes the result, and percent-escapes it so the + and / that base64 produces do not collide with URL syntax. The output goes after #code=, trailed by a couple of flags that tell the reader how to unpack it: j for JSON, z for gzip, a for autorun. A link livelink writes ends in &jz. webR’s own share button writes &mz, using msgpack instead of JSON, and livelink reads both, so a link made by either tool decodes the same way.

A left-to-right pipeline: R code becomes a JSON array of file objects, which is gzip-compressed, then base64-encoded, then written into the #code= fragment of a webR URL. Opening the URL runs the code in the browser.

Your code is serialized to JSON, gzipped, base64-encoded, and placed in the fragment of a webR URL after the #. Opening the link runs it in the browser.

A left-to-right pipeline: R code becomes a JSON array of file objects, which is gzip-compressed, then base64-encoded, then written into the #code= fragment of a webR URL. Opening the URL runs the code in the browser.

Your code is serialized to JSON, gzipped, base64-encoded, and placed in the fragment of a webR URL after the #. Opening the link runs it in the browser.

Shinylive links use a different envelope for the same idea. In place of gzip and base64 they use LZ-string compression tuned for URLs, and the engine is written into the path, shinylive.io/r/ versus shinylive.io/py/, so a link carries its own language. The principle does not change. The whole app, serialized and compressed, sits in the part of the URL that never leaves the browser.

The round trip loses nothing

Because the payload is entirely self-contained, reading a link back is the pipeline in reverse, and it needs no network at all. decode_webr_link() percent-decodes, base64-decodes, gunzips, and parses the JSON back into files on your disk. preview_webr_link() does the same in memory and writes nothing, which is the honest way to open a link a stranger sent you. Look before you extract.

library(livelink)

url <- as.character(webr_repl_link({ data(mtcars); plot(mtcars$mpg, mtcars$wt) }))

# Inspect what is inside without writing a single file
preview_webr_link(url)

What you put in is what you get back, byte for byte, including multi-file projects and non-ASCII text.

A round trip drawn as a loop between two nodes. On the left, one project of three files is expanded to show its contents: main.R sources utils.R, utils.R defines a run function returning 42, and README.md holds an Analysis heading. The project flows out along an encode arc labeled webr_repl_project into a single URL on the right, shown in one address field as the host webr.r-wasm.org/latest/ followed by its #code fragment ending in the jz flags; a dotted rule labeled gzip + base64 links that fragment to the same three files listed below as chips under the heading the same files, inside. A decode arc labeled decode_webr_link returns to the expanded files. Both directions are pure computation with no server and no network.

Encoding and decoding are inverse operations. The files you put into a link are the files you get back, byte for byte, with no server and no network.

A round trip drawn as a loop between two nodes. On the left, one project of three files is expanded to show its contents: main.R sources utils.R, utils.R defines a run function returning 42, and README.md holds an Analysis heading. The project flows out along an encode arc labeled webr_repl_project into a single URL on the right, shown in one address field as the host webr.r-wasm.org/latest/ followed by its #code fragment ending in the jz flags; a dotted rule labeled gzip + base64 links that fragment to the same three files listed below as chips under the heading the same files, inside. A decode arc labeled decode_webr_link returns to the expanded files. Both directions are pure computation with no server and no network.

Encoding and decoding are inverse operations. The files you put into a link are the files you get back, byte for byte, with no server and no network.

There is a pleasing detail here. Source code compresses well, so a link usually comes out smaller than the code it carries, since gzip more than pays for what base64 costs. The exception is binary data, which does not compress and which base64 inflates by a third, so a PNG does not belong in a link. Have the code fetch it, or point at it. And this decoding is the same “no secrets” property peeky trades on, except here it is a feature you were handed on purpose. Any livelink, and any link webR itself produced, opens back up into the exact files that made it.

Try it yourself

livelink is on GitHub now, with a CRAN submission in review, so install the current version with pak:

# install.packages("pak")
pak::pak("coatless-rpkg/livelink")

Once it clears CRAN, install.packages("livelink") will work as well.

For the release notes and the full function tour, see the announcement post. The package documentation carries vignettes on getting started, multi-file projects and Shiny apps, decoding and previewing links, links inside documents, and teaching with livelink. If you find a link livelink cannot make or cannot read, that is a bug report I would love to see.

Acknowledgements

Thanks to George Stagg for webR and its browser REPL, and to Winston Chang for the Shinylive share-URL feature. livelink writes to the share formats they built and opens in the runtimes they maintain, so it is mostly a friendly wrapper around a good idea they had. Pyodide is its own remarkable project, and it is what runs the Python side of Shinylive. Thanks, too, to peeky for stating the other half of this idea plainly enough that the feature became obvious. And for turning a braced R expression into clean, verbatim source, livelink borrows reprex’s stringify_expression(), which is MIT licensed. livelink is one step, and webrarian is the next, already taking shape.