Wednesday, February 5, 2025

Ruby on Rails on WebAssembly, the full-stack in-browser journey  |  web.dev

Web DevelopmentRuby on Rails on WebAssembly, the full-stack in-browser journey  |  web.dev


Published: January 31, 2025

Imagine running a fully functional blog in your browser—not just the frontend,
but the backend, too. No servers or clouds involved—just you, your browser,
and… WebAssembly! By allowing server-side
frameworks to run locally, WebAssembly is blurring the boundaries of classic
web development and opening up exciting new possibilities.
In this post, Vladimir Dementyev (Head of Backend at Evil
Martians) shares the progress on making Ruby on
Rails Wasm- and browser-ready:

  • How to bring Rails into the browser in 15 minutes.
  • Behind the scenes of Rails wasmification.
  • Future of Rails and Wasm.

Ruby on Rails’ famous “blog in 15 minutes” now running right in your browser

Ruby on Rails is a web framework focused on developer productivity and shipping
things fast. It’s the technology used by industry leaders such as
GitHub
and
Shopify.
The popularity of the framework began many years ago with the release of the
famous
“How to build a blog in 15 minutes”
video published by David Heinemeier Hansson (or DHH). Back in 2005, it was
unimaginable to build a fully working web application in such a short time. It
felt like magic!

Today, I’d like to bring this magical feeling back by creating a Rails
application that runs fully in your browser. Your journey starts with creating a
basic Rails application the usual way, and then packaging it for Wasm.

Background: a “blog in 15 minutes” on the command line

Assuming you have
Ruby and Ruby on Rails installed on your machine,
you start with creating a new Ruby on Rails application and scaffolding some
functionality (just like in the original “blog in 15 minutes” video):


$ rails new --css=tailwind web_dev_blog

  create  .ruby-version
  ...

$ cd web_dev_blog

$ bin/rails generate scaffold Post title:string date:date body:text

  create    db/migrate/20241217183624_create_posts.rb
  create    app/models/post.rb
  ...

$ bin/rails db:migrate

== 20241217183624 CreatePosts: migrating ====================
-- create_table(:posts)
   -> 0.0017s
== 20241217183624 CreatePosts: migrated (0.0018s) ===========

Without even touching the codebase, you can now run the application and see it
in action:

$ bin/dev

=> Booting Puma
=> Rails 8.0.1 application starting in development
...
* Listening on http://127.0.0.1:3000

Now, you can open your blog at
http://localhost:3000/posts and start writing
posts!

A Ruby on Rails blog launched from the command line running in the browser.

You have a very bare-bone, but functional blog application built in minutes.
It’s a full-stack, server-controlled application: you have a database
(SQLite) to keep your data, a web server to handle
HTTP requests (Puma), and a Ruby program to keep your
business logic, provide UI, and process user interactions. Finally, there is a
thin layer of JavaScript (Turbo) to streamline
the browsing experience.

The official Rails demo continues in the direction of deploying this application
onto a bare metal server and, thus, making it production-ready. Your journey
will continue in the opposite direction: instead of putting your application
somewhere far away, you’ll “deploy” it locally.

Next level: a “blog in 15 minutes” in Wasm

Since the addition of WebAssembly, browsers became capable of running not only
JavaScript code, but any code compilable into Wasm. And Ruby is not an
exception. Surely, Rails is more than Ruby, but before digging into the
differences, let us continue the demo and wasmify (a verb coined by the
wasmify-rails library) the Rails
application!

You only need to execute a few commands to compile your blog application into a
Wasm module and run it in the browser.

First, you install the wasmify-rails library using Bundler (the npm of Ruby)
and run its generator using the Rails CLI:

$ bundle add wasmify-rails

$ bin/rails wasmify:rails

  create  config/wasmify.yml
  create  config/environments/wasm.rb
  ...
  info   The application is prepared for Wasm-ificaiton!

The wasmify:rails command configures a dedicated “wasm”
execution environment
(in addition to the default “development”, “test”, and “production”
environments) and installs the required dependencies. For a greenfield Rails
application, this is enough to make it Wasm-ready.

Next, build the core Wasm module containing the Ruby runtime, the standard
library, and all the application dependencies:

$ bin/rails wasmify:build

==> RubyWasm::BuildSource(3.3) -- Building
...
==> RubyWasm::CrossRubyProduct(ruby-3.3-wasm32-unknown-wasip1-full-4aaed4fbda7afe0bdf4e22167afd101e) -- done in 47.37s
INFO: Packaging gem: rake-13.2.1
...
INFO: Packaging gem: wasmify-rails-0.2.0
INFO: Packaging setup.rb: bundle/setup.rb
INFO: Size: 73.77 MB

This step can take some time: you must build Ruby from source to properly link
native extensions (written in C) from the third-party libraries. This
(temporary) drawback is covered later in the post.

The compiled Wasm module is just a foundation for your application. You must
also pack the application code itself and all the assets (for example,
images, CSS, JavaScript). Before doing the packing, create a basic launcher
application that could be used to run the wasmified Rails in the browser. For
that, there’s also a generator command:

$ bin/rails wasmify:pwa

  create  pwa
  create  pwa/boot.html
  create  pwa/boot.js
  ...
  prepend  config/wasmify.yml

The previous command generates a minimal PWA application built with
Vite that can be used locally to test the compiled Rails
Wasm module or be deployed statically to distribute the app.

Now, with the launcher, all you need is to pack the whole application into a
single Wasm binary:

$ bin/rails wasmify:pack
...
Packed the application to pwa/app.wasm
Size: 76.2 MB

That’s it! Run the launcher app and see your Rails blogging application running
fully within the browser:

$ cd pwa/

$ yarn dev

  VITE v4.5.5  ready in 290 ms

    Local:   http://localhost:5173/

Go to http://localhost:5173, wait a bit for the
“Launch” button to become active, and click it—enjoy working with the Rails app
running locally in your browser!

A Ruby on Rails blog launched from a browser tab running in another browser tab.

Doesn’t it feel like magic running a monolithic server-side application not just
on your machine but within the browser sandbox? For me (even though I’m the
“sorcerer”), it still looks like a fantasy. But there is no magic involved, only
the progress of technology.

Demo

You can experience the demo embedded in the article or launch the demo in a
standalone window. Check out
the source code on
GitHub.

Behind the scenes of Rails on Wasm

To better understand the challenges (and solutions) of packing a server-side
application into a Wasm module, the rest of this post explains the components
that are part of this architecture.

A web application depends on many more things than just a programming language
used to write the application code. Each component must also be brought to
your_ local deployment environment_—the browser. What’s exciting about the “blog
in 15 minutes” demo, is that this can be achieved without rewriting the
application code. The same code was used to run the application in a classic,
server-side mode and in the browser.

The components that make up a Ruby on Rails app: a web server, a database, a queue, and storage. Plus the core Ruby components: the gems, native extensions, system tools, and the Ruby VM.

A framework, like Ruby on Rails, gives you an interface, an abstraction to
communicate with infrastructure components. The following section discusses
how you can employ the framework architecture to serve the somewhat esoteric
local serving needs.

The foundation: ruby.wasm

Ruby became officially
Wasm-ready in 2022
(since version 3.2.0) meaning that the C source code could be compiled to Wasm
and bring a Ruby VM anywhere you want. The
ruby.wasm project ships precompiled modules
and JavaScript bindings to run Ruby in the browser (or any other JavaScript
runtime). The ruby:wasm project also comes with the build tools that lets You
build a custom Ruby version with additional dependencies—this is very
important for projects relying on libraries with C extensions. Yes, you can
compile native extensions into Wasm, too! (Well, not any extension yet, but most
of them).

Currently, Ruby fully supports the WebAssembly System Interface, WASI
0.1. WASI
0.2 which includes the Component
Model is already in the alpha
state and a few steps from completion.Once WASI 0.2 is supported it will
eliminate the current need of recompiling the whole language every time you need
to add new native dependencies: they could be componentized.

As a side effect, the Component Model should also help with reducing the bundle
size.
You can learn more about the ruby.wasm development and progress from the
What you can do with Ruby on WebAssembly
talk.

So, the Ruby part of the Wasm equation is solved. But Rails as a web framework
needs all of the components shown in the previous diagram. Read on to learn how
to put other components into the browser and link them together in Rails.

Connect to a database running in the browser

SQLite3 comes with an official Wasm
distribution and a corresponding
JavaScript wrapper,
therefore is ready to be embedded in-browser. PostgreSQL for Wasm is available
through the PGlite project. Therefore, you only need to
figure out how to connect to the in-browser database from the Rails on Wasm
application.

A component, or sub-framework, of Rails responsible for data modeling and
database interactions is called Active Record (yes, named after the ORM design
pattern). Active Record
abstracts away the actual SQL-speaking database implementation from the
application code through the database adapters. Out of the box, Rails gives you
SQLite3, PostgreSQL, and MySQL adapters. However, they all assume connecting to
real databases available over the network. To overcome this, you can write
your own adapters to connect to local, in-browser databases!

This is how SQLite3 Wasm and PGlite adapters implemented as a part of the
Wasmify Rails project are created:

  • The adapter class inherits from the corresponding built-in adapter (for
    example, class PGliteAdapter < PostgreSQLAdapter), so you can re-use the
    actual query preparation and results parsing logic.
  • Instead of the low-level database connection, you use an external
    interface
    object that lives in the JavaScript runtime—a bridge between a
    Rails Wasm module and a database.

For example, here is the bridge implementation for SQLite3 Wasm:

export function registerSQLiteWasmInterface(worker, db, opts = ) 
  const name = opts.name 

From the application perspective, the shift from a real database to an
in-browser one is just a matter of configuration:

# config/database.yml
development:
  adapter: sqlite3

production:
  adapter: sqlite3

wasm:
  adapter: sqlite3_wasm
  js_interface: "sqliteForRails"

Working with a local database doesn’t require a lot of effort. However, if data
synchronization with some central source of truth is required, then you may
face a challenge of a higher level. This question is out of the scope of this
post (hint: check out the
Rails on PGlite and ElectricSQL demo).

Service worker as a web server

Another essential component of any web application is a web server. Users
interact with web applications using HTTP requests. Thus, you need a way to route
HTTP requests triggered by navigation or form submissions to your Wasm module.
Luckily, the browser has an answer for that—service workers.

A service worker is a special kind of a Web Worker that acts as a proxy between
the JavaScript application and the network. It can intercept requests and
manipulate them, for example: serve cached data, redirect to other URLs or… to
Wasm modules! Here is a sketch of a service working serving requests using a
Rails application running in Wasm:

// 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)
  );
);

The “fetch” is triggered every time a request is made by the browser. You can
obtain the request information (URL, HTTP headers, body) and construct your own
request object.

Rails, like most Ruby web applications, relies on the Rack
interface for working with HTTP requests. Rack
interface describes the format of the request and response objects as well as
the interface of the underlying HTTP handler (application). You can express
these properties as follows:

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, ..., [...]]

If you found the request format familiar, then you’ve probably worked with
CGI back in the
days.

The RackHandler JavaScript object is responsible for converting requests and
responses between JavaScript and Ruby realms. Given that Rack is used by most
Ruby web applications, the implementation becomes universal, not Rails-specific.
The
actual implementation
is too long to post here though.

A service worker is one of the key integral points of an in-browser web
application. It’s not only an HTTP proxy, but also a caching layer and a
network switcher (that is, you can build a local-first or offline-capable
application). This is also a component that can help you serve user-uploaded
files.

Keep file uploads in the browser

One of the first additional features to implement in your fresh blog application
is likely to be support for file uploads, or more specifically, attaching images
to posts. To achieve this, you need a way to store and serve files.

In Rails, the part of the framework responsible for dealing with file uploads is
called Active
Storage. Active
Storage gives developers abstractions and interfaces to work with files without
thinking about the low-level storage mechanism. No matter where you store your
files, on a hard drive or in the cloud, the application code stays unaware of
it.

Similarly to Active Record, in order to support a custom storage mechanism, all
you need is to implement a corresponding storage service adapter. Where to
store files in the browser?

The traditional option is to use a database. Yes, you can store files as blobs
in the database, no additional infrastructure components required. And there is
already a ready-made plugin for that in Rails,
Active Storage Database.
However, serving files stored in a database through the Rails application
running within WebAssembly is not ideal because it involves rounds of
(de-)serialization that are not free.

A better and more browser-optimized solution would be to use File System APIs
and process file uploads and server uploaded files directly from the service
worker. A perfect candidate for such infrastructure is the
OPFS
(origin private file system), a very recent browser API that will definitely
play an important role for the future in-browser applications.

What Rails and Wasm can achieve together

I’m pretty sure you’ve been asking yourself this question as you started reading
the article: why run a server-side framework in the browser?
The idea of a framework or a library being server-side (or client-side) is just
a label. Good code and, especially, a good abstraction works everywhere. Labels
shouldn’t stop you from exploring new possibilities and pushing boundaries of
the framework (for example, Ruby on Rails) as well as the boundaries of the
runtime (WebAssembly). Both could benefit from such unconventional use cases.

There are plenty of conventional, or practical, use cases, too.

First, bringing the framework to the browser opens enormous learning and
prototyping opportunities
. Imagine being able to play with libraries, plugins,
and patterns right in your browser and together with other people.
Stackblitz made this possible for JavaScript
frameworks. Another example is a WordPress
Playground that made it possible
to play with WordPress themes without leaving the web page. Wasm could enable
something similar for Ruby and its ecosystem.

There’s a special case of in-browser coding especially useful to open source
developers—triaging and debugging issues. Again, StackBlitz made this a
thing for JavaScript projects: you create a minimal reproduction script, point
at the link in a GitHub Issue, and spare maintainers the time on reproducing
your scenario. And, actually, it’s already started happening in Ruby thanks to
the RunRuby.dev project (here is an
example issue
resolved with the in-browser reproduction).

Another use case is offline-capable (or offline-aware) applications.
Offline-capable applications that usually work using the network, but when there
is no connection, they stay usable. For example, an email client that lets you
search through your inbox while offline. Or, a music library application with
the “Store on device” capability, so your favourite music keeps beating even if
there is no network connection. Both examples depend on the data stored
locally
, not just using a cache as with classic PWAs.

Finally, building local (or desktop) applications with Rails also makes sense,
because the productivity the framework gives you doesn’t depend on the runtime.
Full-featured frameworks suit well for building personal data- and logic-heavy
applications. And using Wasm as a portable distribution format is also a viable
option.

It’s just the beginning of this Rails on Wasm journey. You can learn more about
the challenges and solutions in the
Ruby on Rails on WebAssembly
ebook (which, by the way, is an offline-capable Rails application itself).

Check out our other content

Check out other tags:

Most Popular Articles