0.0
No release in over 3 years
A Sinatra extension that implements the full Inertia.js v2 wire protocol: page-object responses, version mismatch detection (409 + X-Inertia-Location), partial reloads, deferred / lazy / always / optional / merge props, encrypted history, redirect 303 handling, and error/flash session sweeps. Pure Sinatra-compatible: depends only on `sinatra` and `rack`. Runs on MRI Ruby and on the homura Cloudflare Workers + Opal stack.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

>= 2.0, < 4.0
>= 3.0, < 5.0
 Project Readme

homura mascot

homura

A platform for running real Ruby applications on Cloudflare Workers.

Live site: https://homura.kazu-san.workers.dev

⚠️ Status — active development. Do not use in production yet. homura is pre-1.0 and changes hard between releases. Public surfaces (gem APIs, CLI, scaffolder layout, build artifacts) can shift in a breaking way at any 0.x bump. Pin exact gem versions if you experiment, expect to chase the next minor, and treat the live demos at *.kazu-san.workers.dev as dogfooding fixtures rather than SLA targets.

# app.rb — yes, it really is plain Sinatra.
require 'sinatra'
require 'sequel'

get '/' do
  'Hello from Ruby, running on Cloudflare Workers.'
end

get '/users' do
  db = Sequel.connect(adapter: :d1, d1: d1)
  content_type :json
  db[:users].order(:id).all.to_json
end
# wrangler.toml — exactly what Cloudflare expects.
name = "myapp"
main = "build/worker.entrypoint.mjs"
compatibility_date = "2026-04-27"
compatibility_flags = ["nodejs_compat"]

There is no shim, no transpiled Sinatra-lookalike. require 'sinatra' loads the canonical Sinatra port; redirect '/' raises Sinatra's own HaltResponse; erb :index, locals: { user: u } resolves locals the way the docs say it does. Modular apps use the same class App < Sinatra::Base

  • config.ru + run App shape upstream documents — see examples/todo/ and the canonical site/ homura.kazu-san.workers.dev source. The whole pipeline exists so Ruby people can keep writing Ruby and ship it to the Cloudflare edge.

Rack-only quick start

Sinatra is optional. A plain Rack app can run directly on the same Workers adapter:

# config.ru
run ->(env) {
  [
    200,
    { 'content-type' => 'text/plain; charset=utf-8' },
    ["Hello from Rack on Cloudflare Workers\n"]
  ]
}

See examples/rack/ for the deployed Rack-only fixture.


Why homura

Cloudflare Workers does not run a Ruby VM. It runs JavaScript on V8, with no filesystem, no eval-from-string, no native extensions. The standard Ruby stack — Sinatra, Sequel, SecureRandom, ERB — assumes all of those.

homura is the glue that closes that gap:

  • Real Sinatra DSLget, post, before, after, helpers, halt, redirect, erb with locals: and layout, Sinatra::Base inheritance exactly as upstream documents.
  • Edge SQLSequel.connect(adapter: :d1, d1: d1) works against Cloudflare D1; migrations are written in Sequel DSL and compiled to wrangler-compatible SQL. Or skip the ORM and use the db.execute(...) wrapper directly.
  • All the bindings — D1, KV, R2, Workers AI, Queues, Scheduled, Durable Objects, Vectorize. They surface as ordinary Ruby helpers such as db, d1, kv, bucket, ai, send_email, jobs_queue, and durable_object(:counter, 'global').
  • Ordinary distribution — four gems on RubyGems. No path:, no submodules, no clone of homura required.
  • Async without ceremony — the build step absorbs Workers' promise-shaped binding calls so user source stays sync-looking, like the Ruby you would write on CRuby. db.execute(...) reads like sqlite3-ruby, not like a coroutine.

If a stock Sinatra-on-Workers idiom doesn't behave the way Ruby developers expect, that is a bug in homura, not a quirk of the stack. The examples/ directory is the contract.


How it works

                         CRuby (build host)               Cloudflare Workers (V8)
                ┌────────────────────────────────┐        ┌──────────────────────────┐
 your Ruby ───► │  bundle exec rake build         │ ─────► │   build/worker.entrypoint.mjs  │
 your views ───►│   ├─ Opal compile (Ruby → JS)   │        │   (loaded by wrangler)   │
 your migrate ─►│   ├─ ERB precompile             │        │   ├─ homura runtime      │
                │   ├─ public/ asset embed        │        │   ├─ your compiled app   │
                │   └─ auto-await pass            │        │   └─ binding shims       │
                └────────────────────────────────┘        └──────────────────────────┘

Four gems own the work:

Gem Responsibility
opal-homura Patched Opal compiler — turns Ruby into the JavaScript that V8 runs. Keep require: 'opal'.
homura-runtime Worker entrypoint, Rack adapter, Cloudflare binding wrappers (D1/KV/R2/AI/Queue), build pipeline (homura build).
sinatra-homura Sinatra port + Opal-compatibility patches, scaffolder (homura new), JWT / Scheduled / Queue helpers, ERB precompiler.
sequel-d1 Sequel adapter for Cloudflare D1, migration compiler (homura db:migrate:*).

The build output is a single build/worker.entrypoint.mjs plus the embedded asset bundle. wrangler deploy ships that file straight to the edge.


Quick start: a new project

Prerequisites: Ruby 3.4+, Node 20+, wrangler (or just npx wrangler).

# Install the scaffolder.
gem install sinatra-homura

# Generate a project (add --with-db for D1 + Sequel).
homura new myapp
cd myapp

# Install dependencies.
bundle install
npm install

# Build the Worker bundle.
bundle exec rake build

# Run wrangler dev locally.
bundle exec rake dev
# → http://127.0.0.1:8787

homura new --with-db myapp additionally writes a Sequel migration under db/migrate/, declares a D1 binding in wrangler.toml, and adds db:migrate:compile, db:migrate:local, and db:migrate:remote Rake tasks. The local development cycle is:

bundle exec rake db:migrate:local   # apply migrations to local D1 (sqlite shim)
bundle exec rake build              # rebuild after editing Ruby
bundle exec rake dev                # wrangler dev, hot-reloads on rebuild

Production deploy:

npx wrangler d1 create myapp                       # one-time
# paste the new database_id into wrangler.toml
bundle exec rake db:migrate:remote                 # apply to remote D1
bundle exec rake deploy                            # wrangler deploy

Adding homura to an existing project

If you already have Ruby and want to ship it to Workers:

  1. Pin the four gems in your Gemfile:

    source 'https://rubygems.org'
    ruby '>= 3.4.0'
    
    gem 'rake'
    gem 'opal-homura',    '= 1.8.3.rc1.5', require: 'opal'
    gem 'homura-runtime', '~> 0.3'
    gem 'sinatra-homura', '~> 0.3'
    gem 'sequel-d1',      '~> 0.3'   # only if you want D1 / Sequel
  2. Keep your Sinatra app exactly as you wrote it. Classic style (require 'sinatra' + top-level get '/' do ... end) and modular style (require 'sinatra/base' + class App < Sinatra::Base + config.ru with run App) both work as upstream documents. You do not swap the require line for a Cloudflare-flavoured one — sinatra-homura wires the Workers runtime into the standard Sinatra entry points automatically.

  3. Add wrangler.toml pointing main at build/worker.entrypoint.mjs and declaring the bindings you need (D1 / KV / R2 / AI / Queue).

  4. Build and run with bundle exec rake build && bundle exec rake dev.

For an existing classic-style Sinatra app, the smallest path is to copy examples/classic-top-sinatra/ and merge your routes in. For a modular-style app, start from examples/todo/ (no ORM) or examples/todo-orm/ (Sequel + D1).

The Workers gotchas section below lists the small set of places where the Workers runtime forces a divergence from CRuby.


Examples

examples/ contains twelve fully-working applications, each one a standalone project that depends on the published gems only — no path: references back to the monorepo. They are also the regression fixtures behind the latest gem releases.

Example Live What it shows
sinatra https://sinatra.kazu-san.workers.dev/ The classic Sinatra README snippet — require 'sinatra' + get '/frank-says'. Single app.rb, no D1, no views.
rack https://rack.kazu-san.workers.dev/ Direct Rack response triples with run ->(env) { ... }; no Sinatra require.
classic-top-sinatra https://classic-top-sinatra.kazu-san.workers.dev/ Same shape as sinatra but with content_type :json + a JSON route, to dogfood the classic top-level DSL.
sinatra-with-db https://sinatra-with-db.kazu-san.workers.dev/ Smallest D1-backed Sinatra: Sequel.connect(adapter: :d1, d1: d1), one route, one migration.
sinatra-with-email https://sinatra-with-email.kazu-san.workers.dev/ Phase 17.5 auto-await demo — POST /send over the SEND_EMAIL Cloudflare Email binding with sync-shaped Ruby source.
todo-simple https://todo-simple.kazu-san.workers.dev/ The smallest stateful example. One app.rb, no views/, no D1 — HTML written as Ruby heredocs. The thing to copy when "how little does homura need" is the question.
todo https://todo.kazu-san.workers.dev/ D1-backed CRUD without an ORM — db.execute / db.execute_insert directly.
todo-orm https://todo-orm.kazu-san.workers.dev/ The same TODO app, this time through sequel-d1: migrations, dataset chains, .first / .update.
auth-otp https://auth-otp.kazu-san.workers.dev/login Email OTP login. Sends through mailpit in development; HMAC-signed session cookie; full headed Playwright E2E in rake e2e:headed.
blog https://blog.kazu-san.workers.dev/ A small blog: index / detail / new / proper 404 / delete. Demonstrates async-route status preservation and <%= h(post[:body]).gsub("\n", "<br>") %>.
inertia-todo https://inertia-todo.kazu-san.workers.dev/ A thin SPA via Inertia.js + Vue 3, with Sinatra serving page props. Client-side JS lives in public/assets/.
hotwire-todo https://hotwire-todo.kazu-san.workers.dev/ Turbo Streams (server-rendered partials over Accept negotiation) + a tiny Stimulus controller for autofocus.

See examples/README.md for the full index with URLs and per-app feature notes.


Known gotchas (and where they are handled)

The Workers runtime forces a few real divergences from CRuby. homura absorbs each of them in the gem layer so application code does not have to:

  • No String#<<. Opal Strings are JS strings, so any code that builds output through mutation (host = String.new; host << '...') breaks. sinatra-homura rebuilds Sinatra::Helpers#uri / redirect / content_type with +-style concatenation, so redirect to('/') works the way Sinatra docs say.
  • No binding.eval. Workers blocks new Function(string), which ERB uses for template rendering. homura-runtime precompiles every views/*.erb to a Ruby method at build time and dispatches erb :name there. erb :_partial, locals: { t: t } resolves bare t via a template-locals stack + Sinatra::Base#method_missing.
  • No filesystem. public/ is bundled at build time and served from memory by a Rack middleware homura installs.
  • String == Symbol under Opal. Sequel::LiteralString was being caught by Sequel's case v when Symbol branch and emitted as a backtick-quoted identifier. sequel-d1 reorders the literal_append branches so update(done: Sequel.lit('1 - done')) lands in literal_literal_string_append.
  • Async at the edge. D1 / KV / fetch are all promise-shaped. The build pipeline runs an auto-await pass for registered async methods (db.execute, kv.get, ai.run, …), so route bodies stay sync-shaped.

If you find an idiom that should "just work" but does not, it belongs on this list. File an issue.


AI / agent support

homura ships agent-discoverable docs so Claude / Copilot / Cursor can pick the right gem and follow the canonical install/build flow without a human in the loop.

Install the skill:

# GitHub Copilot (or any host that reads `gh skill`)
gh skill install kazuph/homura homura-workers-gems --agent github-copilot --scope user

# Claude Code (project-scoped is recommended)
gh skill install kazuph/homura homura-workers-gems --agent claude-code --scope project

# npm-based installer (also works for Claude / Copilot)
npx skills add kazuph/homura --skill homura-workers-gems -a claude-code

Repository layout

gems/                 # The four published gems live here
  homura-runtime/     # core runtime + build pipeline
  sinatra-homura/     # Sinatra port + Opal patches + scaffolder
  sequel-d1/          # Sequel D1 adapter + migration compiler
  sinatra-inertia/    # Inertia.js v2 adapter for Sinatra
vendor/opal-gem/      # Source of opal-homura (the patched Opal fork)
vendor/                 # Bundled Sinatra / Rack / Sequel / etc. for Workers
examples/             # Standalone example apps consuming the released gems
site/                 # The canonical homura.kazu-san.workers.dev application
                      # (Sinatra app, ERB views, public/, wrangler.toml)
test/                 # Gem-side smoke + Ruby tests (`npm test`)
docs/                 # Long-form documentation
skills/               # Installable agent skills

The gems/* directories and vendor/opal-gem/ are the shipping surface. site/ is the dogfooding application that proves the gems work end-to-end on Cloudflare Workers; it lives in this repo as a sibling of the gem code so runtime regressions show up the moment they ship.


Repo maintenance

Tracked first-party Ruby is mechanically formatted with fables-tales/rubyfmt, the same formatter lineage Stripe describes in its rubyfmt rollout story. Vendored and generated paths stay outside that boundary.

npm run format:ruby:install
npm run format:ruby
npm run format:ruby:check
ruby bin/install-git-hooks

The formatter scope is defined once in bin/first-party-ruby-files and shared by the formatter and ruby -c CI gates.

ruby bin/install-rubyfmt downloads the pinned rubyfmt release used by CI into build/tools/. If you already manage rubyfmt globally (for example with brew install rubyfmt), bin/format-ruby will use that too.

ruby bin/install-git-hooks configures core.hooksPath=.githooks for the current clone. The pre-commit hook auto-formats staged first-party Ruby so local commits fail earlier, while CI remains the source of truth.


License

MIT. See LICENSE.