Rails, Wasm, and the browser as a server

WebAssembly has made it possible to run server-side frameworks directly in the browser, eliminating the need for a remote server or cloud infrastructure. The Ruby on Rails project has been working toward making this a reality, and recent progress shows a full Rails application can now run entirely inside a browser tab.

Vladimir Dementyev, Head of Backend at Evil Martians, has demonstrated how to take the classic "blog in 15 minutes" Rails demo and run it entirely in-browser via WebAssembly, without changing the application code. All the familiar components—database, web server, and application logic—run locally inside the browser sandbox.

From localhost to browser: A quick walkthrough

Starting with a standard Rails installation, the process mirrors the traditional Rails tutorial. After creating a new application and scaffolding blog posts, you get a bare-bones but functional full-stack app that runs on your local machine with SQLite for data storage, Puma handling HTTP requests, and Turbo providing a JavaScript layer.

The key difference comes next: instead of deploying the application to a server, you "deploy" it into the browser using the wasmify-rails library. The process takes just a few steps:

  1. Install the gem and run the generator to set up a dedicated "wasm" execution environment.
  2. Build the core Wasm module containing Ruby runtime, standard library, and all application dependencies. This step compiles Ruby from source to properly link native C extensions.
  3. Generate and run a launcher—a minimal Vite-based PWA that boots the compiled Wasm module.

After the build completes, you navigate to the launcher URL, wait for the "Launch" button to activate, and the Rails app loads fully in the browser. Posts can be created, edited, and viewed without any server communication.

The architecture behind in-browser Rails

A web application depends on more than just its programming language. The Ruby runtime, database, HTTP server, and application code all need to be brought into the browser environment. The challenge is mapping each infrastructure component to a browser-compatible alternative.

Ruby runtime on Wasm

Ruby has been officially Wasm-ready since version 3.2.0, and the ruby.wasm project provides precompiled modules and JavaScript bindings. Crucially, its build tools also allow custom Ruby compilations with additional native extensions—necessary for Rails dependencies that rely on C code.

Ruby currently supports WASI 0.1, with WASI 0.2 support nearing completion. Once WASI 0.2 (which includes the Component Model) is fully implemented, it will eliminate the need to recompile the entire language when adding new native dependencies.

Database connections without a database server

SQLite3 ships an official Wasm distribution with a JavaScript wrapper, and PGlite provides the same for PostgreSQL. But Rails' Active Record assumes connecting to a real database over a network.

The wasmify-rails project solves this with custom database adapters that inherit from corresponding built-in adapters—for example, PGliteAdapter inheriting from PostgreSQLAdapter—so existing query preparation and result-parsing logic remain intact. The difference lies in a lower-level connection layer: instead of a socket to a real database, these adapters use an external interface object that bridges the Rails Wasm module to the in-browser database running in the JavaScript environment.

From the application's perspective, switching from a remote database to an in-browser one is just a configuration change, not a code change:

# database.yml excerpt for Wasm environment

The simpler data persistence story is well-handled, but true data synchronization with a central source remains an open area for further exploration—something hinted at by a related Rails on PGlite demo integrating ElectricSQL.

Performance notes and future directions

Building the initial Wasm module takes time: since native extensions must be linked at compile time, Rails dependencies often mean building Ruby from source first. That's a temporary drawback—partially a result of the current WASI 0.1 limitation. With full WASI 0.2 and the Component Model, the engine and dependencies should be individually available as reusable modules, shrinking both build times and bundle sizes.

You can try the demo yourself via the embedded browser experience, open it in a standalone window, or explore the source code on GitHub.

Running a monolith locally in the browser still feels aspirational to developers—yet the demo shows it's technically achievable today with no application code changes. The required ingredients are a Ruby runtime compiled to Wasm, an in-browser database adapter, and a lightweight launcher.

Bridging HTTP with a service worker

Any web application also needs a server to handle HTTP requests triggered by navigation or form submissions. In the browser, that role falls to a service worker — a special type of Web Worker that proxies between the JavaScript application and the network. Instead of sending requests outward, a service worker can intercept them and pass the request data directly to a Wasm module running Rails:

// The vm variable holds a reference to the Wasm module with a
// Ruby VM initialized
let vm;
// The db variable holds a reference to the in-browser
// database interface
let db;

const initVM = async (progress, opts = {}) => {
  if (vm) return vm;
  if (!db) {
    await initDB(progress);
  }
  vm = await initRailsVM("/app.wasm");
  return vm;
};

const rackHandler = new RackHandler(initVM});

self.addEventListener("fetch", (event) => {
  // ...
  return event.respondWith(
    rackHandler.handle(event.request)
  );
});

This "fetch" event fires for every browser request. You can read the request's URL, headers, and body, and construct your own request object from that information.

Speaking Rack

Like most Ruby web frameworks, Rails depends on the Rack interface to abstract HTTP requests and responses. This interface defines the shape of request and response objects, as well as the contract for the underlying HTTP handler:

request = {
   "REQUEST_METHOD" => "GET",
   "SCRIPT_NAME"    => "",
   "SERVER_NAME"  => "localhost",
   "SERVER_PORT" => "3000",
   "PATH_INFO"      => "/posts"
}

handler = proc do |env|
  [
    200,
    {"Content-Type" => "text/html"},
    ["<!doctype html><html><body>Hello Web!</body></html>"]
  ]
end

handler.call(request) #=> [200, {...}, [...]]

Veterans may recognize this format from the days of CGI.

A RackHandler JavaScript object then bridges the two worlds, translating requests and responses between JavaScript and Ruby. Because nearly all Ruby web applications depend on Rack, this handler is universal rather than Rails-specific. The full implementation is lengthier than what fits here.

Beyond routing HTTP, the service worker doubles as a caching layer and a network switcher. That makes it possible to build local-first or fully offline applications, and it can also serve user-uploaded content from local storage.

Handling uploads locally

File uploads — for instance, attaching images to a blog post — require both storage and a way to serve those files back. Rails abstracts this behind Active Storage, which lets developers work with files without caring where they physically live.

To plug in a new storage backend, you implement a storage service adapter, just as you would for Active Record. A straightforward option in the browser is storing blobs in a database; the Active Storage Database gem already provides this. However, serving database-backed files through Rails in Wasm requires repeated (de-)serialization that isn't cheap.

A more browser-native approach stores and streams files directly from the service worker using the origin private file system (OPFS), a recent browser API that is expected to become central to in-browser applications.

Practical reasons to run Rails in Wasm

The "server-side" label on a framework like Rails is mostly convention. Good abstractions work regardless of runtime, and pushing the framework into WebAssembly exercises both Rails and the Wasm ecosystem in useful ways.

Learning and debugging

Running the framework in the browser creates significant learning and prototyping opportunities. Developers can experiment with libraries, plugins, and patterns directly in a browser tab, even collaboratively. Stackblitz has done this for JavaScript frameworks, and the WordPress Playground lets users poke at themes without leaving the page. Wasm can bring the same to Ruby.

For open source maintainers, a special case is triaging issues. StackBlitz popularized the pattern: a contributor adds a minimal reproduction script to a GitHub issue, saving maintainers the effort of setting up an environment. Ruby is already seeing this via RunRuby.dev — see this example issue resolved through in-browser reproduction.

Offline and local applications

There's also room for offline-capable applications that rely on locally stored data rather than a pure network cache. An email client that stays searchable without a connection, or a music library with a "store on device" toggle, both fit this pattern.

Finally, building local (or desktop) applications with Rails is a natural fit: framework productivity doesn't disappear when runtime changes. Full-featured frameworks excel at data- and logic-heavy personal apps, and Wasm provides a portable .wasm distribution format.

This is only the beginning of Rails on Wasm. For deeper coverage of the challenges, see the Ruby on Rails on WebAssembly ebook — itself an offline-capable Rails application.