Blog Demo
A classic Rails blog with articles and comments. The same code runs on Rails, in browsers with IndexedDB, on Node.js with SQLite, and on edge platforms.
Table of Contents
- Create the App
- Run with Rails
- Run in the Browser
- Run on Node.js
- Deploy to Vercel
- Deploy to Cloudflare
- The Code
- What This Demo Shows
- What Works Differently
- What Doesn’t Work
- Next Steps
Create the App
curl -sL https://raw.githubusercontent.com/ruby2js/ruby2js/master/test/blog/create-blog | bash -s blog
cd blog
This creates a Rails app with:
- Article scaffold — title, body, CRUD operations
- Comment scaffold — nested under articles,
belongs_to :article - Associations —
has_many :comments, dependent: :destroy - Validations —
validates :title, presence: true,validates :body, length: { minimum: 10 } - Nested routes —
resources :articles { resources :comments } - Tailwind CSS — styled forms and layouts
- Sample data — seeded articles and comments
Run with Rails
First, verify it works as a standard Rails app:
RAILS_ENV=production bin/rails db:prepare
bin/rails server -e production
Open http://localhost:3000. Browse articles. Add comments. Delete them. This is Rails as you know it—CRuby, SQLite, the full stack.
Run in the Browser
Stop Rails. Run the same app in your browser:
bin/juntos dev -d dexie
Open http://localhost:3000. Same blog. Same articles. Same comments. But now:
- No Ruby runtime — the browser runs transpiled JavaScript
- IndexedDB storage — data persists in your browser via Dexie
- Hot reload — edit a Ruby file, save, browser refreshes
- Auto-migrations — database schema updates automatically on startup
Debugging in DevTools
Open DevTools. In the Sources panel, find your Ruby files—app/models/article.rb, app/controllers/articles_controller.rb. Set breakpoints on Ruby lines. Step through Ruby code. Inspect variables with Ruby names.
The Console shows Rails-style logging:
Article Create {title: "Hello", body: "World", created_at: "..."}
Article Update {id: 1, title: "Updated", updated_at: "..."}
Run on Node.js
bin/juntos db:prepare -d sqlite
bin/juntos up -d sqlite
Open http://localhost:3000. Same blog—but now Node.js serves requests, and better-sqlite3 provides the database.
The db:prepare command runs migrations and seeds if the database is fresh. The up command builds and starts the server.
Other runtimes work too:
bin/juntos up -t bun -d sqlite # Bun runtime
bin/juntos up -t deno -d postgres # Deno with PostgreSQL
Deploy to Vercel
bin/juntos db:prepare -d neon
bin/juntos deploy -d neon
Prerequisites:
- Vercel CLI —
npm i -g vercelandvercel login - Create a Vercel project — run
vercelonce to link - Create a Neon database
- Connect database — add
DATABASE_URLas a Vercel environment variable - Local environment — copy credentials to
.env.localfor migrations
Like Rails, migrations run separately from deployment. The db:prepare command applies migrations and seeds if fresh. The deploy command builds and deploys.
Deploy to Cloudflare
bin/juntos db:prepare -d d1
bin/juntos deploy -d d1
Prerequisites:
- Wrangler CLI —
npm i -g wranglerandwrangler login
The db:prepare command creates the D1 database (if not already set up), runs migrations, and seeds if fresh. The database ID is saved to .env.local automatically as D1_DATABASE_ID (for development) or D1_DATABASE_ID_PRODUCTION (for production).
The Code
The code is idiomatic Rails. Try it — edit the Ruby to see how models transpile:
class Article < ApplicationRecord
has_many :comments, dependent: :destroy
validates :title, presence: true
validates :body, presence: true, length: { minimum: 10 }
end
Try it — controllers also transpile directly:
class ArticlesController < ApplicationController
before_action :set_article, only: %i[show edit update destroy]
def index
@articles = Article.all
end
def show
end
def create
@article = Article.new(article_params)
if @article.save
redirect_to @article
else
render :new, status: :unprocessable_entity
end
end
private
def set_article
@article = Article.find(params[:id])
end
end
Nothing special. Nothing modified for transpilation. Standard Rails conventions work.
What This Demo Shows
Model Layer
has_manyandbelongs_toassociationsdependent: :destroyfor cascading deletesvalidateswithpresenceandlength
Controller Layer
before_actioncallbacks- Instance variable assignment (
@article) - Standard CRUD actions
redirect_toandrender- Strong parameters (
article_params)
View Layer
- ERB templates with
<%= %>and<% %> link_to,button_to,form_with- Nested forms for comments
- Partials (
_article.html.erb,_comment.html.erb) - Layouts with
yield
Routes
resources :articles- Nested
resources :comments root "articles#index"- Generated path helpers (
article_path,new_article_path)
What Works Differently
- Migrations — In browsers, migrations run automatically on startup
- Database — IndexedDB in browsers, SQLite/PostgreSQL on servers
- No ActiveRecord queries — Use
where,find,all—no raw SQL
What Doesn’t Work
- Complex associations —
has_many :throughis limited - Callbacks —
after_saveworks;around_*callbacks don’t - Scopes — Lambda scopes need explicit conversion
Next Steps
- Try the Chat Demo for real-time features
- Read the Architecture to understand what gets generated
- Check Deployment Guides for detailed platform setup