Rails to the Edge and Beyond
Rails remains the fastest way to go from idea to working application. Convention over configuration, omakase defaults, a decade of refinements—nothing else matches its productivity for CRUD applications.
Serverless is the future of deployment. No servers to manage, automatic scaling, pay-per-request pricing, global distribution. But serverless usually means rewriting for proprietary platforms—and once you're in, switching is costly.
Rails requires Ruby. Serverless platforms run JavaScript. Until now, you had to choose.
What if you didn't have to choose?
Ruby2JS and the Transpilation Spectrum
Ruby runs in more places than ever. Each implementation makes different tradeoffs between semantic fidelity and ecosystem integration:
| Implementation | Semantics | Ecosystem |
|---|---|---|
| CRuby | Reference Ruby | C extensions |
| JRuby | Minor threading differences | JVM libraries |
| Ruby WASM | Full Ruby semantics | Awkward JS interop |
| Opal | Minor behavior differences | Ruby-ish JS abstractions |
| Ruby2JS | No metaprogramming | Native JavaScript |
Ruby2JS produces JavaScript that is JavaScript. A Ruby array becomes a JS array. A Ruby class becomes a JS class. You lose runtime metaprogramming—no method_missing—but you gain seamless access to the JavaScript ecosystem without impedance mismatch.
Introducing Juntos
Juntos is a set of Ruby2JS filters and a small runtime that implement a Rails-compatible framework for JavaScript platforms.
Rails is built on metaprogramming. Write has_many :comments and Rails generates methods at runtime. Juntos inverts this: instead of runtime generation, filters pre-compute what Rails would generate at transpile time. The Rails DSL is finite and declarative—filters recognize every pattern and expand it statically.
The result: idiomatic JavaScript that runs anywhere.
# app/models/article.rb
class Article < ApplicationRecord
has_many :comments, dependent: :destroy
validates :title, presence: true
end
becomes:
// dist/app/models/article.js
class Article extends ApplicationRecord {
static associations = { comments: { type: "hasMany", dependent: "destroy" } };
static validations = { title: { presence: true } };
}
Same models. Same controllers. Same ERB templates. Different programming language.
The Demo
Create the classic Rails blog—articles with nested comments, validations, Tailwind CSS:
curl -sL https://raw.githubusercontent.com/ruby2js/ruby2js/master/test/blog/create-blog | bash
cd blog
Run it with Rails:
bin/rails db:prepare
bin/rails server
Run it in the browser with IndexedDB:
bin/juntos dev -d dexie
Run it on Node.js with SQLite:
bin/juntos migrate -d sqlite
bin/juntos up -d sqlite
Deploy to Vercel Edge with Neon:
bin/juntos migrate -d neon
bin/juntos deploy -d neon
Deploy to Cloudflare Workers with D1:
bin/juntos migrate -d d1
bin/juntos deploy -d d1
Five runtimes. Same code. The database determines the deployment target—Neon implies Vercel, D1 implies Cloudflare. In practice, you configure config/database.yml per environment and just use -e:
development:
adapter: dexie
production:
adapter: neon
bin/juntos deploy -e production
The Juntos documentation has the full tutorial with prerequisites and deployment guides.
Different Platforms, Different Capabilities
Juntos currently targets six JavaScript platforms. The same code runs on all of them, but they aren't equivalent:
| Platform | Strengths | Constraints |
|---|---|---|
| Browser | Full sourcemaps, offline-capable, no server needed | No SMTP, no server-side APIs |
| Node/Bun/Deno | Full capabilities + ecosystem (Puppeteer, LangChain, TensorFlow) | Requires hosting |
| Vercel/Cloudflare | Global edge, auto-scaling, pay-per-request | No filesystem, no jobs, no websockets |
For a CRUD blog, any platform works—pick based on deployment preferences. For applications that need browser automation, AI/ML pipelines, or background processing, these differences guide your choice.
Why This Matters
No lock-in. The generated dist/ directory is a complete JavaScript application. You could fork it and continue development in pure JS—no Ruby required. The output isn't compiled bytecode; it's readable code you can understand and modify. Or stay in Ruby and switch from Browser to Node to Vercel without changing any code.
Rails fidelity. Other frameworks are Rails-inspired. Juntos aims for Rails patterns exactly. Rails developers feel at home. Rails documentation mostly applies.
Multi-target. No other approach offers the same code running in browsers with IndexedDB, on Node.js with SQLite, and on edge functions with serverless databases.
The filters handle the translation—different runtime APIs, different database bindings, different deployment configs. Same Rails code.
What's Possible
This is early. Rough edges remain. But the core works—not as a proof of concept, but as deployable applications. The architecture is proven; what remains is polish and expanding coverage.
The architecture extends beyond what's implemented today. An Action Mailer filter could transform UserMailer.welcome(@user).deliver_later into Resend or SendGrid API calls. An Active Storage filter could transform @article.image.attach(params[:image]) into Cloudflare R2 or Vercel Blob uploads. A Vite plugin could make Ruby a first-class frontend language—hot module replacement, tree shaking, and integration with Vue, Svelte, or Astro components.
The Rails DSL becomes the abstraction; the JavaScript ecosystem provides the implementation. See the roadmap for what's possible.
Learn More
Juntos—Spanish for "together"—is what emerges when Ruby2JS transpiles Rails patterns. Ruby and JavaScript. Rails conventions and serverless platforms. Your productivity and the modern deployment story. Together—you don't have to choose.
- Juntos Documentation — Getting started, CLI reference, architecture
- Deployment Guides — Browser, Node.js, Vercel, Cloudflare
- Source on GitHub
Ruby2JS is open source: github.com/ruby2js/ruby2js