A Ruby DSL web framework for Cloudflare Workers, powered by mruby + WASI
Homura brings the expressiveness of Ruby to edge computing. Write your routing logic in Ruby, deploy to Cloudflare Workers. The framework compiles mruby to WebAssembly (WASI target) and communicates with the JS runtime via MessagePack IPC.
Request → index.ts (JS glue) → mruby.wasm (MessagePack IPC) → routes.rb (Ruby DSL)
↓ ↓
D1 / KV bindings Homura framework
(JS-side execution) (routing, context, ORM)
Ruby side handles: routing, request parsing, business logic, ORM queries, response building
JS side handles: WASM lifecycle, D1/KV binding execution, JSX template rendering
Communication uses MessagePack with a continuation-loop pattern for async I/O (D1 queries trigger ContinueRequest, JS executes the SQL, then resumes Ruby with results)
cd examples/webapp
npm install
npm run bundle:ruby # Bundle lib/homura.rb + app/routes.rb into src/ruby-bundle.ts
npm run dev # Start wrangler dev on port 8787
Homuraでは /api/* を含むアプリ挙動は原則すべて examples/webapp/app/routes.rb で定義します。
Homura's routing DSL is inspired by Sinatra . Here's what's implemented:
$app. get "/hello/:name" do |c |
c . json ( { message : "Hello, #{ c . params [ :name ] } !" } )
end
$app. post "/users" do |c |
data = c . json_body
c . json ( { created : data } , status : 201 )
end
Sinatra
Homura
Status
get '/path'
$app.get "/path" do |c| ... end
Supported
post '/path'
$app.post "/path" do |c| ... end
Supported
put '/path'
$app.put "/path" do |c| ... end
Supported
patch '/path'
$app.patch "/path" do |c| ... end
Supported
delete '/path'
$app.delete "/path" do |c| ... end
Supported
options '/path'
$app.options "/path" do |c| ... end
Supported
Route params '/users/:id'
c.params[:id]
Supported
Wildcard '/files/*'
c.params[:_wildcard]
Supported
Optional params '/users/:id?'
Supported
Supported
Regex constraints '/users/:id{[0-9]+}'
Supported
Supported
$app. helpers do
def format_date ( time )
time . strftime ( "%Y-%m-%d" )
end
end
# Available in all route handlers via `c.format_date(time)`
Sinatra
Homura
Status
helpers do ... end
$app.helpers do ... end
Supported
helpers MyModule
Not supported (block-only)
Partial
$app. configure do |app |
app . set :app_name , "My App"
app . enable :logging
end
$app. configure :production do |app |
app . disable :debug
end
Sinatra
Homura
Status
configure do ... end
$app.configure do |app| ... end
Supported
configure :production do ... end
Environment-scoped configure
Supported
set :key, value
$app.set :key, value
Supported
enable :feature
$app.enable :feature
Supported
disable :feature
$app.disable :feature
Supported
settings.key
$app.settings[:key]
Supported
Sinatra
Homura
Status
params[:name]
c.params[:name]
Supported
request.body
c.req.text / c.req.json
Supported
request.path
c.req.path
Supported
request.request_method
c.req.method
Supported
request.query_string
c.req.query / c.req.query(:key)
Supported
request.url
c.req.url
Supported
request.env['HTTP_X_FOO']
c.req.header("X-Foo")
Supported
halt 403
c.halt(403)
Supported
halt 200, 'OK'
c.halt(200, nil, "OK")
Supported
$app. get "/api/data" do |c |
c . json ( { key : "value" } ) # Content-Type: application/json
end
$app. get "/page" do |c |
c . html ( "<h1>Hello</h1>" ) # Content-Type: text/html
end
$app. get "/greeting" do |c |
c . text ( "Hello, World!" ) # Content-Type: text/plain
end
$app. get "/home" do |c |
c . jsx ( "home" , { title : "Top" } ) # JSX server-side rendering
end
Sinatra
Homura
Status
content_type :json; body data.to_json
c.json(data)
Supported
erb :template
c.jsx("template", props)
Supported (JSX instead of ERB)
redirect '/path'
c.redirect("/path")
Supported
status 201
c.status(201)
Supported
headers "X-Custom" => "value"
c.header("X-Custom", "value")
Supported
cookies[:name]
c.cookie("name")
Supported
response.set_cookie
c.set_cookie(name, value, opts)
Supported
session[:key]
c.session[:key] (cookie-based)
Supported
# Global middleware (runs on all routes)
$app. use do |ctx , nxt |
ctx . header ( "X-Powered-By" , "Homura" )
nxt . call
end
# Route-scoped middleware
$app. use "/admin" do |ctx , nxt |
# Auth check for /admin routes only
nxt . call
end
Sinatra
Homura
Status
before do ... end
$app.use do |ctx, nxt| ... end
Supported (Hono-style)
after do ... end
$app.after do ... end
Supported
Route-scoped middleware
$app.use "/path" do ... end
Supported
$app. not_found do |c |
c . json ( { error : "Not found" } , status : 404 )
end
$app. on_error do |e , c |
c . json ( { error : e . message } , status : 500 )
end
Sinatra
Homura
Status
not_found do ... end
$app.not_found do ... end
Supported
error do ... end
$app.on_error do ... end
Supported
error 404 do ... end
Not supported (use not_found)
Not yet
Sinatra Feature
Status
before '/path' do ... end (filter syntax)
Use $app.use "/path" instead
Named routes (url(:name))
Not yet
Streaming responses
Not yet
WebSocket
Not yet
File uploads / multipart
Not yet
error 404 do (status-specific error blocks)
Not yet
Template engines (ERB, Haml, Slim)
JSX only
Class-based app (class MyApp < Sinatra::Base)
Global $app only
Homura::Model (ActiveRecord-Style ORM)
Homura::Model provides an ActiveRecord-inspired ORM for Cloudflare D1 (SQLite). It's defined in lib/homura_model.rb.
class Article < Homura ::Model
table :articles
column :id , :integer
column :title , :string
column :body , :string
column :author , :string
column :published , :boolean
validates :title , presence : true
validates :author , presence : true
end
# Find by ID
article = Article . find ( c . db , 1 )
# Where conditions (Hash only - no raw SQL for security)
articles = Article . where ( author : "Alice" ) . all ( c . db )
# Chaining: where + order + limit + offset
articles = Article . where ( published : true )
. order ( "created_at DESC" )
. limit ( 10 )
. offset ( 20 )
. all ( c . db )
# Count
count = Article . where ( published : true ) . count ( c . db )
# First record
article = Article . where ( title : "Hello" ) . first ( c . db )
# Scope + enum + pluck
titles = Article . published . order ( "id DESC" ) . pluck ( :title , c . db )
Article . statuses #=> { "draft" => 0, "published" => 1 }
ActiveRecord
Homura::Model
Status
Article.find(1)
Article.find(c.db, 1)
Supported
Article.where(key: val)
Article.where(key: val)
Supported (Hash only)
Article.where("sql")
Not supported (security)
Intentionally omitted
Article.where.not(key: val)
Article.where.not(key: val)
Supported
.order("col DESC")
.order("col DESC")
Supported
.limit(10)
.limit(10)
Supported
.offset(20)
.offset(20)
Supported
.all
.all(c.db)
Supported (requires db arg)
.first
.first(c.db)
Supported
.last
.last(c.db)
Supported
.count
.count(c.db)
Supported
.pluck(:field)
.pluck(:field, c.db)
Supported
.ids
.ids(c.db)
Supported
.exists?
.exists?(c.db)
Supported
.find_by(...)
find_by(c.db, ...)
Supported
.find_or_create_by(...)
find_or_create_by(c.db, ...)
Supported
.select/.group/.having/.distinct
Supported
Supported
# Create
article = Article . create ( c . db , title : "Hello" , author : "Alice" )
# Read
article = Article . find ( c . db , 1 )
article . title #=> "Hello"
# Update
article . update_attrs ( c . db , title : "Updated Title" )
# or
article . title = "Updated Title"
article . save ( c . db )
# Delete
article . destroy ( c . db )
ActiveRecord
Homura::Model
Status
Model.create(attrs)
Model.create(c.db, attrs)
Supported
record.save
record.save(c.db)
Supported
record.update(attrs)
record.update_attrs(c.db, attrs)
Supported
record.destroy
record.destroy(c.db)
Supported
record.valid?
record.valid?
Supported
record.errors
record.errors (array of strings)
Supported
record.persisted?
record.persisted?
Supported
record.to_h
record.to_h
Supported
record.attribute = val
record.attribute = val (via method_missing)
Supported
class Article < Homura ::Model
validates :title , presence : true
validates :author , presence : true
validates :title , length : { minimum : 3 , maximum : 120 }
validates :published , inclusion : { in : [ true , false ] }
validate :title_must_be_clean
end
article = Article . new ( title : "" , author : "Alice" )
article . valid? #=> false
article . errors #=> ["title can't be blank"]
ActiveRecord
Homura::Model
Status
validates :field, presence: true
Supported
Supported
validates :field, length: {...}
Supported
Supported
validates :field, format: { with: /regex/ }
Supported
Supported
validates :field, inclusion/exclusion
Supported
Supported
validates :field, numericality: {...}
Supported
Supported
Custom validations
validate :method_name
Supported
Associations, Callbacks, Dirty Tracking, Scopes, Enum
class Author < Homura ::Model
table :authors
has_many :articles
end
class Article < Homura ::Model
table :articles
belongs_to :author
scope :published , -> { where ( published : true ) }
enum :status , [ :draft , :published ]
before_save :normalize_title
end
Associations : has_many, belongs_to, has_one register metadata and load related records with record.association(c.db).
Callbacks : before/after_validation, before/after_save, before/after_create, before/after_update, before/after_destroy.
Dirty tracking : changed?, changes, changed_attributes, title_changed?, title_was.
Scopes : class-level scope :published, -> { where(published: true) } returning chainable queries.
Enum : integer-backed mappings with getters, predicates, bang setters, and class helpers like Article.statuses.
The ORM automatically casts column values based on declared types:
Column Type
Ruby Type
DB Storage
:integer
Integer
INTEGER
:boolean
true/false
INTEGER (0/1)
:string
String
TEXT
c.db required : Unlike ActiveRecord's global connection, Homura passes the D1 database handle explicitly. This is because Cloudflare Workers' D1 is request-scoped.
No raw SQL in where() : where("1=1; DROP TABLE x") is intentionally rejected. Only Hash conditions are accepted to prevent SQL injection.
Minimal associations : has_many, belongs_to, and has_one are intentionally lightweight. through, polymorphic, autosave, and inverse helpers are out of scope.
No migrations : Use D1's native SQL migration files (migrations/*.sql).
Minimal lifecycle hooks : callbacks return false to halt and are kept intentionally simple for mruby compatibility.
Explicit query safety : query builders stay column-whitelisted and avoid raw SQL fragments in where.
# Via ORM
articles = Article . where ( published : true ) . all ( c . db )
# Direct SQL (low-level)
result = c . db . all ( "SELECT * FROM articles WHERE published = ?" , [ 1 ] )
row = c . db . get ( "SELECT * FROM articles WHERE id = ?" , [ 1 ] )
c . db . run ( "INSERT INTO articles (title) VALUES (?)" , [ "Hello" ] )
value = c . kv_get ( "my-key" )
c . kv_put ( "my-key" , "my-value" )
c . kv_delete ( "my-key" )
The WASI build includes 30 mrbgems. 17 were added in this release:
Gem
Category
What It Enables
mruby-time
Standard lib
Time.now, timestamps, time arithmetic
mruby-random
Standard lib
Random.new, rand(), secure random generation
mruby-pack
Standard lib
Array#pack, String#unpack - binary encoding/decoding
mruby-eval
Metaprogramming
eval, instance_eval, module_eval
mruby-metaprog
Metaprogramming
define_method, define_singleton_method, send, respond_to?
mruby-binding
Metaprogramming
Binding objects for closures
mruby-enumerator
Collections
Enumerator, each_with_object, map, select
mruby-enum-lazy
Collections
Lazy enumerators for memory-efficient pipelines
mruby-set
Collections
Set class for unique collections
mruby-data
Data types
Immutable value objects (Data.define)
mruby-bigint
Numeric
Arbitrary-precision integers
mruby-rational
Numeric
Rational number arithmetic
mruby-fiber
Concurrency
Fiber-based coroutines
mruby-enum-chain
Collections
Enumerator::Chain for chaining enumerators
mruby-catch
Control flow
catch/throw for non-local jumps
mruby-compar-ext
Core ext
Comparable#clamp
mruby-numeric-ext
Core ext
Extended numeric methods
Previously included (13 gems): mruby-sprintf, mruby-math, mruby-struct, mruby-enum-ext, mruby-string-ext, mruby-array-ext, mruby-hash-ext, mruby-range-ext, mruby-proc-ext, mruby-symbol-ext, mruby-object-ext, mruby-kernel-ext, mruby-class-ext, mruby-method, mruby-error, mruby-compiler.
Each example demonstrates specific mrbgems and framework features:
Example
Description
Key mrbgems / Features
webapp
Todo App with full CRUD
D1, JSX templates, per-request VM lifecycle
url-shortener
Base62 URL shortening with click tracking
mruby-random, mruby-pack, KV storage
json-transform
JSON data pipeline (filter, map, group, dedupe)
mruby-enumerator, mruby-enum-lazy, mruby-set
template-engine
Variable interpolation + HTML escaping
mruby-metaprog (define_singleton_method)
time-tracker
Event logging with time-windowed stats
mruby-time, mruby-pack, mruby-bigint, D1
dsl-api
DSL-driven auto CRUD generation
Homura::Model ORM, mruby-metaprog, D1
cd examples/< name>
npm install
npm run bundle:ruby # Bundles lib/homura.rb + app/routes.rb
npm run dev # Starts wrangler dev server
# From repository root
cd e2e
npm install
BASE_URL=http://127.0.0.1:< port> npx playwright test < example> .spec.ts
cd mruby
make setup # Clone mruby 3.3.0 source
make # Build mruby.wasm (~790KB)
homura/
├── lib/
│ ├── homura.rb # Framework core (routing, context, middleware, Sinatra-compat)
│ └── homura_model.rb # Homura::Model ORM
├── mruby/
│ ├── build_config.rb # 30 mrbgems for WASI build
│ └── src/homura_entry.c # C API (MessagePack IPC)
├── examples/
│ ├── webapp/ # Todo app (D1 + JSX)
│ ├── url-shortener/ # Base62 + KV
│ ├── json-transform/ # Enumerable pipeline
│ ├── template-engine/ # Metaprogramming template
│ ├── time-tracker/ # Time + bigint + D1
│ └── dsl-api/ # ORM auto-CRUD
└── e2e/ # Playwright E2E tests
Hono - Ultrafast web framework for the Edge
Sinatra - DSL for quickly creating web applications in Ruby
ActiveRecord - ORM pattern for Ruby
mruby - Lightweight implementation of Ruby
MIT