Blog Demo
A Rails blog with articles, comments, and real-time updates. The same code runs on Rails, in browsers with IndexedDB, on Node.js with SQLite, and on edge platforms. Open multiple browser tabs to see changes sync instantly.
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
Try it live — no install required.
To run locally:
npx github:ruby2js/juntos --demo 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 } - Real-time updates —
broadcasts_tofor live create/update/destroy - 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
broadcasts_to ->(article) { "articles" }, inserts_by: :prepend
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 deletesvalidateswithpresenceandlengthincludes(:comments)for eager loading (prevents N+1 queries)
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)
Real-Time Updates
broadcasts_tofor automatic create/update/destroy broadcaststurbo_stream_fromto subscribe views to channelsafter_create_commit/after_destroy_commitcallbacks- Comment count updates live on the index page
What Works Differently
- Migrations — In browsers, migrations run automatically on startup
- Database — IndexedDB in browsers, SQLite/PostgreSQL on servers
- Real-time transport — Browser tabs use BroadcastChannel API; Node/Bun/Deno use Action Cable; Cloudflare uses Durable Objects with WebSockets
- ActiveRecord queries — Use
where,find,all, chainable queries, and basic raw SQL conditions (see Active Record for the full query interface)
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 multi-user real-time messaging
- Read the Architecture to understand what gets generated
- Check Deployment Guides for detailed platform setup