Ruby applications, right in the browser

A proposal for making Ruby applications portable through WebAssembly, packaged dependencies and explicit browser adapters.

Research checked 16 October 2026 · Architecture proposal · Published 16 October 2026

The interpreter already exists

ruby.wasm ports CRuby to WebAssembly. Ruby code executes in a Ruby interpreter compiled for Wasm; it is not automatically translated into JavaScript. The project supplies browser packages, standard-library profiles, JavaScript interoperability and tooling for packaging application files. Its current README shows Ruby 4.0 browser assets and the wasm32-unknown-wasip1 target.

The same README documents missing Ruby Thread APIs and native networking in its current WASI build. Those statements describe that build, not every WASI proposal or future implementation. A browser host can expose Fetch through JavaScript without making arbitrary Ruby socket libraries work. Pin the runtime, package versions and build target in each compatibility report.

CRuby’s WebAssembly port notes also identify process creation as unsupported. A shell command or external executable needs an alternative implementation. This is a deployment constraint to detect before offering the application to users.

Rails has also crossed this boundary. Vladimir Dementyev’s 2025 Rails-on-WebAssembly walkthrough demonstrates a packaged blog running locally in the browser. It is evidence that a useful Rails workflow can work, not a certification of every application, gem or deployment configuration.

Wasmify Rails supplies build/packaging tasks, a browser application template, SQLite and PGlite Active Record adapters, upload middleware and null implementations for selected server services. Its README still lists Active Storage OPFS, background jobs and WASI Preview 2 support as roadmap items. The changelog records Rack integration and compatibility work. These are useful starting points to evaluate, not a reason to label missing features complete.

Keep Ruby; adapt the environment

Our proposal has four parts. Package a known Ruby runtime with an audited dependency set. Run the interpreter away from the interface thread. Expose a small host interface for permitted browser capabilities. Keep trusted shared services on the server when their responsibilities require it.

  1. Build artifact: a versioned runtime, Ruby sources, selected gems, prebuilt assets and public seed data. Produce a manifest with hashes, licenses and compatibility results. Exclude credentials, production databases, private configuration and build caches by construction.
  2. Application worker: boot one Ruby instance and accept bounded messages with request IDs, cancellation and errors. Keep the visible interface responsive. Restarting a worker must not silently discard committed user work.
  3. Adapters: implement files, database access, HTTP, logging and UI communication through narrow Ruby interfaces. Provide native-server and browser implementations where useful. An unsupported operation returns a clear error; it must not pretend a write or email succeeded.
  4. Optional server: retain authorization, shared records, durable jobs and external-service secrets. A hybrid application can perform substantial local work without claiming that every service is offline.

This follows our existing adapter guidance. The portability unit should be a real user workflow, such as “import a CSV, validate rows and export a report,” rather than “the application booted.”

Web Workers use message passing and cannot directly manipulate the document. Put DOM updates on the main thread; pass results rather than thousands of tiny cross-boundary calls. Separate worker runtimes are not Ruby threads sharing one heap. This layout is our proposed default, not a claim that every existing Rails-on-Wasm template uses it.

Start with one VM and serialized application operations. Terminating its worker cancels computation but does not prove that a storage transaction rolled back. Define that boundary explicitly and test it before adding a pool of separate VMs.

A compatibility contract for each application

On narrow screens, scroll the table horizontally to compare all three columns.

Application shapeProposed browser pathProof required
Pure Ruby algorithms and parsersBundle Ruby code and supported libraries; pass data in and results out.All reachable dependencies load; outputs match native Ruby on representative fixtures.
Command-line utilitiesReplace terminal input, arguments and output with forms, selected files and downloads.No hidden subprocess, terminal-control or unrestricted filesystem dependency.
Rack, Sinatra or RodaAdapt browser requests to the Rack call interface.Middleware, body streaming, uploads, redirects and cookie behavior tested for the selected stack.
Rails local demo or single-user toolEvaluate Wasmify packaging and browser database adapters.Boot, CRUD, migrations, reload recovery and every supported user journey pass.
Shared Rails business applicationMove selected computation and local editing to the browser; keep trusted services remote.Server authorization, synchronization conflicts, retries and tenant boundaries remain correct.
Native-heavy or operating-system toolsPort a bounded dependency, replace it, or keep that operation server-side.Target-compatible extensions and semantics demonstrated; unsupported capabilities identified.

“Pure Ruby” describes implementation language, not environmental independence. A pure Ruby gem can still expect a shell, writable system paths or a TCP connection. A native extension must be built for the chosen Wasm runtime and ABI; a Linux shared library cannot simply be copied into the browser. Library portability and host capabilities both matter. Our Ruby and C guide remains useful, but desktop FFI is not a universal escape hatch here.

Wasmify’s dependency-selection and build-verification steps are a practical model: deliberately choose the Wasm dependency group, investigate extension failures and verify the packaged application. Removing a failed gem is only acceptable if the corresponding feature is removed, replaced or deliberately remote. An ignored build error is not compatibility.

Rack is the seam; a browser is not Puma

The Rack specification defines an application object that accepts a request environment and returns status, headers and body. That gives a browser bridge a useful target without opening a listening TCP socket.

A bridge must supply the selected Rack version’s environment and input/error streams, handle body iteration and closure, and translate the response into browser behavior. Use Rack::Lint and fixtures covering binary bodies, encoding, redirects, errors and uploads. Do not advertise raw connection hijacking or streaming semantics that the bridge does not implement.

A service worker can intercept requests within its scope and provide offline caching. That makes it a potential routing layer, separate from the application worker. Its lifecycle is managed by the browser; in-memory state is not durable. Define update activation and cache invalidation so an old page cannot accidentally talk to an incompatible new runtime. The static application still needs an initial delivery origin; offline operation starts only after the necessary assets and data are available.

Prefer an existing tested Rails launcher for the first experiment. Our separate service-worker/application-worker arrangement needs its own implementation and routing tests: direct navigation before the Ruby worker starts, requests after its owner page closes, worker failure and recovery. A diagram of this route does not establish that it works.

JavaScript is the browser adapter

Ruby can reach host APIs through require "js". The official interop guide documents objects, callbacks and awaiting JavaScript promises. Awaiting requires the asynchronous evaluation path, such as RubyVM#evalAsync or the script loader’s async setting. A synchronous Ruby API does not become asynchronous just because its implementation invokes Fetch.

For our application interface, favor coarse operations: read a chosen document, perform a bounded computation, return a result. Keep conversion rules explicit for numbers, byte arrays, exceptions and cancellation. Test loss of precision and large inputs. Do not shuttle whole databases through string conversion for convenience.

Fetch follows browser origin, credential and CORS rules. no-cors does not make a forbidden response readable. An HTTP adapter can call an allowed API; it cannot give a PostgreSQL, Redis or SMTP socket client transparent network access. A server gateway should expose specific authorized operations, not an unrestricted URL proxy.

Three different meanings of “a file”

Packaged Ruby files provide the application’s virtual filesystem. Writable runtime memory can provide scratch space. Durable user documents need a separately selected persistence mechanism. These layers must not be conflated: successfully calling File.write is not evidence that the next browser session will recover the file.

Our proposed storage interface distinguishes immutable application assets, temporary work and committed user data. For user documents, offer explicit import/export; for structured state, evaluate IndexedDB or a browser database. Record the storage version and recovery procedure. Flush and verify critical data before saying “saved.”

The Ruby filesystem and a separate SQLite module do not automatically share files. Connecting them requires an implemented bridge. For the first local database, choose one owner and a single-writer policy; show a clear message if a second tab cannot acquire ownership.

SQLite’s Wasm persistence documentation describes several virtual filesystem options with different concurrency and hosting requirements. Its opfs VFS uses SharedArrayBuffer and cross-origin isolation headers. opfs-sahpool avoids that header requirement but has a different access model. Choose a VFS deliberately and test multi-tab contention, rather than treating OPFS as one interchangeable switch. The relevant synchronous database access runs in workers; this is not a claim that all OPFS APIs are worker-only.

Do not apply SQLite’s persistence behavior automatically to PGlite or to ordinary Ruby files. Verify the chosen database adapter, its own persistence configuration and schema-upgrade behavior. Wasmify’s database adapters do not establish that its planned Active Storage OPFS service is already implemented.

Browser storage can hit quotas or be evicted; users can also clear it. Persistent-storage requests may help, but export and recovery remain product requirements. Probe actual writes, not just API names. If durable storage fails, clearly offer an ephemeral session with download/export, or a remote mode. Never silently downgrade a promised durable workflow.

Background work needs a lifetime

A worker can keep computation off the interface thread. It does not promise completion after the tab closes, the device sleeps or the browser discards the process. A service worker is not a permanently running queue consumer either. Wasmify currently labels background jobs as roadmap.

Our proposal separates cancellable local work from durable obligations. A local conversion can checkpoint, show progress and resume when the application opens. Billing, mail delivery, scheduled processing and shared-data maintenance belong in an authorized server queue with idempotency and observable failures. A null mail adapter is useful for a demo only if the interface explicitly says no email was sent.

The browser is a different trust boundary

Every file and constant delivered to the browser is available to its user. A compiled module is not a vault. Build with synthetic public configuration; scan the artifact inventory, not only the source tree. Keep secret keys, database credentials and privileged service tokens on the server behind our existing secrets interface.

Our proposal retains server-side identity and authorization for shared business data. Client-side validations improve usability but cannot enforce permissions against the person controlling the client. Recheck tenant scope and authorization on every server operation; never trust a locally edited role or a locally generated “approved” flag.

Wasm isolates memory within the host’s rules; imported JavaScript capabilities still matter. A worker does not establish a separate origin or a safe hostile-code sandbox. Run untrusted user programs only within a separately designed isolation boundary, with restricted capabilities, resource limits and cancellation. Do not place that evaluator beside privileged application sessions and assume Wasm solves the problem.

For the proposed normal application, restrict outbound destinations, validate message shapes, avoid HTML injection and minimize third-party scripts. Give users control over local-data deletion and exports. Browser-local processing can reduce server data exposure, but “local” does not automatically mean private or encrypted.

Prove the served runtime can boot

An available WebAssembly API and a valid artifact do not prove that the deployed URL will load. The WebAssembly Web API draft requires streaming compilation responses to be successful, readable and served as application/wasm without parameters. A 200 response containing an HTML fallback is not a runtime. The 16 September 2026 CSP Level 3 draft also governs compilation: wasm-unsafe-eval is narrower than unsafe-eval because it does not enable JavaScript string evaluation. Evaluate the application’s actual policy; this article changes no headers.

Proposed acceptance checks, not executed here: record artifact hashes, loader and browser versions, response headers and active CSP. In isolated fixtures, test wrong MIME, a 200 HTML fallback, an unreadable cross-origin response and a policy that denies compilation. Require a known Ruby fixture to return its expected value and show a useful fallback on failure. Then test offline cold restart, update, and a complete import–compute–export workflow; boot alone proves none of those. No runtime was built or installed for this note.

A small implementation that earns broader claims

  1. First proof: package one pure Ruby CSV validator with public fixtures. Run in a worker, import a file and export a report. Compare results with the native Ruby implementation and prove cancellation works.
  2. Persistence proof: add one selected database backend. Test reload, quota failure, interrupted writes, export/import, schema migration and two-tab behavior. Publish the supported browser matrix.
  3. Rack proof: exercise a tiny app through the bridge with normal, error and binary responses. Document which optional protocol features are absent.
  4. Rails proof: package a small CRUD workflow with audited dependencies, synthetic data and explicit disabled services. Verify it offline after installation, then test upgrade and recovery.
  5. Hybrid proof: connect one authenticated, authorized remote operation. Test expired access, offline retries, conflicts and duplicate submissions. Keep the server’s authority intact.

Before selecting a mode, probe runtime instantiation, worker startup, database initialization, a real persistence round trip and the required network route. Treat cross-origin isolation as a requirement of the selected implementation, not a blanket requirement for Ruby in a browser. Show the user the resulting mode and its limits.

Measure compressed transfer size, cold boot, warm boot, first useful result, peak memory and export latency on a modest mobile device as well as a desktop. Record runtime, browser and app versions. Set budgets before optimizing; this article offers no invented benchmark or universal performance claim.

The useful outcome is a family of explicit compatibility profiles. A local document tool may need no server after installation. A shared Rails system may keep its trusted core on the server while moving expensive or offline-friendly work to the browser. Both can keep Ruby as the application language, with evidence attached to each supported workflow.


Source review: primary project documentation and browser-platform documentation linked beside the claims. Demonstrations cited above were reviewed as published evidence; no Rails/Wasm application was built or executed for this article. All architecture, rollout and test recommendations are our proposal. Versions and roadmap status should be rechecked before implementation.

Provided as is, for information only, with no warranty and no liability. Not legal or professional advice. Security and radio material is for authorized, lawful use only. Full disclaimer.

Visits are logged on our own server: IP address, browser and device details, and pages viewed. We use them to understand traffic and spot attacks, and never share or sell them.